From 9979e34e2cf0c03c5a65bde50832fadc91035635 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 02:19:46 +0100 Subject: [PATCH 1/3] cosmetic fixes --- WordPressKit/StatsServiceRemoteV2.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 1d06aff1..b8da7a1d 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -99,6 +99,7 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { } } +// MARK: - StatsLastPostInsight-specific hack extension StatsServiceRemoteV2 { // "Last Post" Insights are "fun" in the way that they require multiple requests to actually create them, @@ -159,7 +160,6 @@ extension StatsServiceRemoteV2 { } ) } - } // This serves both as a way to get the query properties in a "nice" way, From 47f20c064b70a669da0ae01d742881761ec94256 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 03:58:33 +0100 Subject: [PATCH 2/3] add support for fetching published posts --- WordPressKit.xcodeproj/project.pbxproj | 8 +++ WordPressKit/StatsServiceRemoteV2.swift | 59 ++++++++++++++++++- .../PublishedPostsStatsType.swift | 39 ++++++++++++ .../Mock Data/stats-published-posts.json | 20 +++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 27 +++++++++ 5 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 WordPressKit/Time-based data/PublishedPostsStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-published-posts.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 680021d3..48bf5bc2 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 404057DC221C9FD80060250C /* stats-referrer-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057DB221C9FD70060250C /* stats-referrer-data.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; + 40819773221E10C900A298E4 /* PublishedPostsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */; }; + 40819775221E497D00A298E4 /* stats-published-posts.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819774221E497C00A298E4 /* stats-published-posts.json */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; 40AB1ADA200FED25009B533D /* PluginDirectoryFeedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40AB1AD9200FED25009B533D /* PluginDirectoryFeedPage.swift */; }; 40E4698B2017C2840030DB5F /* plugin-directory-popular.json in Resources */ = {isa = PBXBuildFile; fileRef = 40E4698A2017C2840030DB5F /* plugin-directory-popular.json */; }; @@ -517,6 +519,8 @@ 404057DB221C9FD70060250C /* stats-referrer-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-referrer-data.json"; sourceTree = ""; }; 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDotComFollowersInsight.swift; sourceTree = ""; }; 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAllTimesInsight.swift; sourceTree = ""; }; + 40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedPostsStatsType.swift; sourceTree = ""; }; + 40819774221E497C00A298E4 /* stats-published-posts.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-published-posts.json"; sourceTree = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; 40AB1AD9200FED25009B533D /* PluginDirectoryFeedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryFeedPage.swift; sourceTree = ""; }; 40E4698A2017C2840030DB5F /* plugin-directory-popular.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "plugin-directory-popular.json"; sourceTree = ""; }; @@ -1022,6 +1026,7 @@ 404057D1221C56AB0060250C /* CountryStatsType.swift */, 404057D5221C92660060250C /* ClicksStatsType.swift */, 404057D9221C9D560060250C /* ReferrerStatsType.swift */, + 40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1540,6 +1545,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 40819774221E497C00A298E4 /* stats-published-posts.json */, 404057DB221C9FD70060250C /* stats-referrer-data.json */, 404057D7221C98690060250C /* stats-clicks-data.json */, 404057D3221C5FC30060250C /* stats-countries-data.json */, @@ -2091,6 +2097,7 @@ 74C473C51EF33242009918F2 /* site-active-purchases-two-active-success.json in Resources */, 93AC8ECA1ED32FD000900F5A /* stats-v1.1-country-views-day.json in Resources */, 74C473BF1EF32B64009918F2 /* site-export-bad-json-failure.json in Resources */, + 40819775221E497D00A298E4 /* stats-published-posts.json in Resources */, 74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */, D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */, 740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */, @@ -2261,6 +2268,7 @@ 9F3E0BA22087345F009CB5BA /* ServiceRequest.swift in Sources */, E1A6605F1FD694ED00BAC339 /* PluginDirectoryEntry.swift in Sources */, 7430C9CB1F192F260051B8E6 /* RemoteSourcePostAttribution.m in Sources */, + 40819773221E10C900A298E4 /* PublishedPostsStatsType.swift in Sources */, 742362E11F1025B400BD0A7F /* RemoteMenuItem.m in Sources */, 436D56332118D7AA00CEAA33 /* TransactionsServiceRemote.swift in Sources */, 93BD27721EE737A9002BB00B /* ServiceRemoteWordPressXMLRPC.m in Sources */, diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index b8da7a1d..924d5a5d 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -101,7 +101,6 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { // MARK: - StatsLastPostInsight-specific hack extension StatsServiceRemoteV2 { - // "Last Post" Insights are "fun" in the way that they require multiple requests to actually create them, // so we do this "fun" dance in a separate method. public func getInsight(completion: @escaping ((StatsLastPostInsight?, Error?) -> Void)) { @@ -162,6 +161,64 @@ extension StatsServiceRemoteV2 { } } +// MARK - PublishedPostsStatsType-specific hack +extension StatsServiceRemoteV2 { + + // PublishedPostsStatsType hit a different endpoint and with different parameters + // then the rest of the time-based types — we need tco handle them separately here. + public func getData(for period: StatsPeriodUnit, + endingOn: Date, + limit: Int = 10, + completion: @escaping ((PublishedPostsStatsType?, Error?) -> Void)) { + + let pathComponent = StatsLastPostInsight.pathComponent + + let path = self.path(forEndpoint: "sites/\(siteID)/\(pathComponent)", withVersion: ._1_1) + + let properties = ["number": limit, + "fields": "ID, title, URL", + "after": ISO8601DateFormatter().string(from: startDate(for: period, endDate: endingOn)), + "before": ISO8601DateFormatter().string(from: endingOn)] as [String: AnyObject] + + wordPressComRestApi.GET(path, + parameters: properties, + success: { (response, _) in + guard + let jsonResponse = response as? [String: AnyObject], + let response = PublishedPostsStatsType(date: endingOn, period: period, jsonDictionary: jsonResponse) else { + completion(nil, ResponseError.decodingFailure) + return + } + completion(response, nil) + }, failure: { (error, _) in + completion(nil, error) + } + ) + } + + private func startDate(for period: StatsPeriodUnit, endDate: Date) -> Date { + switch period { + case .day: + return Calendar.autoupdatingCurrent.startOfDay(for: endDate) + case .week: + let weekAgo = Calendar.autoupdatingCurrent.date(byAdding: .day, value: -6, to: endDate)! + return Calendar.autoupdatingCurrent.startOfDay(for: weekAgo) + case .month: + let monthAgo = Calendar.autoupdatingCurrent.date(byAdding: .month, value: -1, to: endDate)! + let firstOfMonth = Calendar.autoupdatingCurrent.date(bySetting: .day, value: 1, of: monthAgo)! + + return Calendar.autoupdatingCurrent.startOfDay(for: firstOfMonth) + case .year: + let yearAgo = Calendar.autoupdatingCurrent.date(byAdding: .year, value: -1, to: endDate)! + let january = Calendar.autoupdatingCurrent.date(bySetting: .month, value: 1, of: yearAgo)! + let jan1 = Calendar.autoupdatingCurrent.date(bySetting: .day, value: 1, of: january)! + + return Calendar.autoupdatingCurrent.startOfDay(for: jan1) + } + } + +} + // This serves both as a way to get the query properties in a "nice" way, // but also as a way to narrow down the generic type in `getInsight(completion:)` method. public protocol InsightProtocol { diff --git a/WordPressKit/Time-based data/PublishedPostsStatsType.swift b/WordPressKit/Time-based data/PublishedPostsStatsType.swift new file mode 100644 index 00000000..7283a1b8 --- /dev/null +++ b/WordPressKit/Time-based data/PublishedPostsStatsType.swift @@ -0,0 +1,39 @@ +public struct PublishedPostsStatsType { + public let periodEndDate: Date + public let period: StatsPeriodUnit + + public let publishedPosts: [StatsTopPost] +} + +extension PublishedPostsStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "posts/" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard let posts = jsonDictionary["posts"] as? [[String: AnyObject]] else { + return nil + } + + self.periodEndDate = date + self.period = period + self.publishedPosts = posts.compactMap { StatsTopPost(postsJSONDictionary:$0) } + } +} + +private extension StatsTopPost { + init?(postsJSONDictionary: [String: AnyObject]) { + guard + let id = postsJSONDictionary["ID"] as? Int, + let title = postsJSONDictionary["title"] as? String, + let urlString = postsJSONDictionary["URL"] as? String + else { + return nil + } + + self.postID = id + self.title = title + self.postURL = URL(string: urlString) + self.viewsCount = 0 + } +} diff --git a/WordPressKitTests/Mock Data/stats-published-posts.json b/WordPressKitTests/Mock Data/stats-published-posts.json new file mode 100644 index 00000000..3d4d4a43 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-published-posts.json @@ -0,0 +1,20 @@ +{ + "found": 3, + "posts": [ + { + "ID": 41038, + "title": "Announcing Newspack by WordPress.com — A New Publishing Solution for News Organizations", + "URL": "http://en.blog.wordpress.com/2019/01/14/newspack-by-wordpress-com/" + }, + { + "ID": 41015, + "title": "Customize Your WordPress.com Dashboard", + "URL": "http://en.blog.wordpress.com/2019/01/08/customize-your-wordpress-com-dashboard/" + }, + { + "ID": 40978, + "title": "Introducing the 2019 ‘Anything Is Possible’ List", + "URL": "http://en.blog.wordpress.com/2019/01/03/introducing-the-2019-anything-is-possible-list/" + } + ], +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 9a9f6ce6..b083ad7f 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -15,6 +15,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getCountriesMockFilename = "stats-countries-data.json" let getClicksMockFilename = "stats-clicks-data.json" let getReferrersMockFilename = "stats-referrer-data.json" + let getPublishedPostsFilename = "stats-published-posts.json" // MARK: - Properties @@ -25,6 +26,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { var siteCountriesDataEndpoint: String { return "sites/\(siteID)/stats/country-views/" } var siteClicksDataEndpoint: String { return "sites/\(siteID)/stats/clicks/" } var siteReferrerDataEndpoint: String { return "sites/\(siteID)/stats/referrers/" } + var sitePublishedPostsEndpoint: String { return "sites/\(siteID)/posts/" } var remote: StatsServiceRemoteV2! @@ -288,4 +290,29 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testsPublishedPosts() { + let expect = expectation(description: "It should return published posts for a specified window") + + stubRemoteResponse(sitePublishedPostsEndpoint, filename: getPublishedPostsFilename, contentType: .ApplicationJSON) + + let jan31 = DateComponents(year: 2019, month: 1, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: jan31)! + + remote.getData(for: .month, endingOn: date) { (publishedPosts: PublishedPostsStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(publishedPosts) + + XCTAssertEqual(publishedPosts?.publishedPosts.count, 3) + + XCTAssertEqual(publishedPosts?.publishedPosts[0].postID, 41038) + XCTAssertEqual(publishedPosts?.publishedPosts[0].postURL, URL(string: "http://en.blog.wordpress.com/2019/01/14/newspack-by-wordpress-com/")) + XCTAssertEqual(publishedPosts?.publishedPosts[0].title, "Announcing Newspack by WordPress.com — A New Publishing Solution for News Organizations") + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } + } From 0683f28f01ec40cd0667ed6cba50a21100f7b6a7 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 21:11:16 +0100 Subject: [PATCH 3/3] fix a typo --- WordPressKit/StatsServiceRemoteV2.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 924d5a5d..9e48c300 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -165,7 +165,7 @@ extension StatsServiceRemoteV2 { extension StatsServiceRemoteV2 { // PublishedPostsStatsType hit a different endpoint and with different parameters - // then the rest of the time-based types — we need tco handle them separately here. + // then the rest of the time-based types — we need to handle them separately here. public func getData(for period: StatsPeriodUnit, endingOn: Date, limit: Int = 10,