From 23bff2835368c3e2eafa3211bbd7e59ab205b120 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 18 Feb 2019 19:25:10 +0100 Subject: [PATCH 01/27] Add basic structure for fetching time-based data --- WordPressKit/StatsServiceRemoteV2.swift | 74 ++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 3d2c14a3..1833f671 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -13,13 +13,22 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { private let siteID: Int private let siteTimezone: TimeZone + private lazy var periodDataQueryDateFormatter: DateFormatter = { + let df = DateFormatter() + df.locale = Locale(identifier: "en_US_POSIX") + df.dateFormat = "yyyy-MM-dd" + return df + }() + public init(wordPressComRestApi api: WordPressComRestApi, siteID: Int, siteTimezone: TimeZone) { self.siteID = siteID self.siteTimezone = siteTimezone super.init(wordPressComRestApi: api) } - + /// Responsible for fetching Stats data for Insights — latest data about a site, + /// in general — not considering a specific slice of time. + /// For a possible set of returned types, see objects that conform to `InsightProtocol`. public func getInsight(completion: @escaping ((InsightType?, Error?) -> Void)) { let properties = InsightType.queryProperties as [String: AnyObject] let pathComponent = InsightType.pathComponent @@ -41,6 +50,40 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { }) } + + /// Used to fetch data about site over a specific timeframe. + /// - parameters: + /// - period: An enum representing whether either a day, a week, a month or a year worth's of data. + /// - endingOn: Date on which the `period` for which data you're interested in **is ending**. + /// e.g. if you want data spanning 11-17 Feb 2019, you should pass in a period of `.week` and an + /// ending date of `Feb 17 2019`. + /// - limit: Limit of how many objects you want returned for your query. Default is `10`. `0` means no limit. + public func getData(for period: StatsPeriodUnit, + endingOn: Date, + limit: Int = 10, + completion: @escaping ((TimeStatsType?, Error?) -> Void)) { + let pathComponent = TimeStatsType.pathComponent + let path = self.path(forEndpoint: "sites/\(siteID)/\(pathComponent)/", withVersion: ._1_1) + + let properties = ["period": period.stringValue, + "date": periodDataQueryDateFormatter.string(from: endingOn), + "max": limit as AnyObject] as [String: AnyObject] + + wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in + guard + let jsonResponse = response as? [String: AnyObject], + let timestats = TimeStatsType(jsonDictionary: jsonResponse) + else { + completion(nil, ResponseError.decodingFailure) + return + } + + completion(timestats, nil) + }, failure: { (error, _) in + completion(nil, error) + }) + } + // "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)) { @@ -111,6 +154,34 @@ public protocol InsightProtocol { init?(jsonDictionary: [String: AnyObject]) } +// naming is hard. +public protocol TimeStatsProtocol { + static var queryProperties: [String: String] { get } + static var pathComponent: String { get } + + var period: StatsPeriodUnit { get } + var periodEndDate: Date { get } + + init?(jsonDictionary: [String: AnyObject]) +} + +// We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed. +// For now we can piggy-back off the old type and add this as an extension. +private extension StatsPeriodUnit { + var stringValue: String { + switch self { + case .day: + return "day" + case .week: + return "week" + case .month: + return "month" + case .year: + return "year" + } + } +} + extension InsightProtocol { // A big chunk of those use the same endpoint and queryProperties.. Let's simplify the protocol conformance in those cases. @@ -124,6 +195,7 @@ extension InsightProtocol { } } + // Swift compiler doesn't like if this is not declared _in this file_, and refuses to compile the project. // I'm guessing this has somethign to do with generic specialisation, but I'm not enough // of a `swiftc` guru to really know. Leaving this in here to appease Swift gods. From c57d4274bbb4eda5f804f2f45af946a24ebb8c42 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 18 Feb 2019 19:50:33 +0100 Subject: [PATCH 02/27] Add `SearchTermStatsType` --- WordPressKit.xcodeproj/project.pbxproj | 12 +++++ WordPressKit/StatsServiceRemoteV2.swift | 26 ++++++++-- .../Time-based data/SearchTermStatsType.swift | 52 +++++++++++++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 WordPressKit/Time-based data/SearchTermStatsType.swift diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index b07961c3..11abe34a 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 240315B0A1D6C2B981572B5B /* Pods_WordPressKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED05C8FF3E61D93CE5BA527E /* Pods_WordPressKitTests.framework */; }; 40247DFA2120D8E100AE1C3C /* AutomatedTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */; }; 40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */; }; + 404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C4221B30400060250C /* SearchTermStatsType.swift */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -490,6 +491,7 @@ 264F5C834541BBF2018F4964 /* Pods-WordPressKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKitTests/Pods-WordPressKitTests.debug.xcconfig"; sourceTree = ""; }; 40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferService.swift; sourceTree = ""; }; 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferStatus.swift; sourceTree = ""; }; + 404057C4221B30400060250C /* SearchTermStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTermStatsType.swift; 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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -987,6 +989,14 @@ name = Frameworks; sourceTree = ""; }; + 404057C3221B30140060250C /* Time-based data */ = { + isa = PBXGroup; + children = ( + 404057C4221B30400060250C /* SearchTermStatsType.swift */, + ); + path = "Time-based data"; + sourceTree = ""; + }; 40414061220F9F2800CF7C5B /* Insights */ = { isa = PBXGroup; children = ( @@ -1006,6 +1016,7 @@ 40B01BF3220E534900036D10 /* V2 */ = { isa = PBXGroup; children = ( + 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, ); name = V2; @@ -2329,6 +2340,7 @@ 40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */, 730E869F21E44EFD00753E1A /* WordPressComServiceRemote+SiteVerticals.swift in Sources */, 740B23C51F17EE8000067A2A /* RemotePost.m in Sources */, + 404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */, 40247DFA2120D8E100AE1C3C /* AutomatedTransferService.swift in Sources */, 74E2295B1F1E77290085F7F2 /* KeyringConnection.swift in Sources */, 74B5F0DA1EF8299B00B411E7 /* BlogServiceRemoteXMLRPC.m in Sources */, diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 1833f671..de5094be 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -72,7 +72,11 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in guard let jsonResponse = response as? [String: AnyObject], - let timestats = TimeStatsType(jsonDictionary: jsonResponse) + let dateString = response["date"] as? String, + let date = self.periodDataQueryDateFormatter.date(from: dateString), + let periodString = response["period"] as? String, + let parsedPeriod = StatsPeriodUnit(string: periodString), + let timestats = TimeStatsType(date: date, period: parsedPeriod, jsonDictionary: jsonResponse) else { completion(nil, ResponseError.decodingFailure) return @@ -156,18 +160,17 @@ public protocol InsightProtocol { // naming is hard. public protocol TimeStatsProtocol { - static var queryProperties: [String: String] { get } static var pathComponent: String { get } var period: StatsPeriodUnit { get } var periodEndDate: Date { get } - init?(jsonDictionary: [String: AnyObject]) + init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) } // We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed. // For now we can piggy-back off the old type and add this as an extension. -private extension StatsPeriodUnit { +fileprivate extension StatsPeriodUnit { var stringValue: String { switch self { case .day: @@ -180,6 +183,21 @@ private extension StatsPeriodUnit { return "year" } } + + init?(string: String) { + switch string { + case "day": + self = .day + case "week": + self = .week + case "month": + self = .month + case "year": + self = .year + default: + return nil + } + } } extension InsightProtocol { diff --git a/WordPressKit/Time-based data/SearchTermStatsType.swift b/WordPressKit/Time-based data/SearchTermStatsType.swift new file mode 100644 index 00000000..44d0ede0 --- /dev/null +++ b/WordPressKit/Time-based data/SearchTermStatsType.swift @@ -0,0 +1,52 @@ +struct SearchTermStatsType { + let period: StatsPeriodUnit + let periodEndDate: Date + + let totalSearchTermsCount: Int + let hiddenSearchTermsCount: Int + let otherSearchTermsCount: Int + let searchTerms: [SearchTerm] +} + + +extension SearchTermStatsType: TimeStatsProtocol { + static var pathComponent: String { + return "stats/search-terms" + } + + init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let days = jsonDictionary["days"] as? [String: AnyObject], + let firstKey = days.keys.first, + let firstDay = days[firstKey] as? [String: AnyObject], + let totalSearchTerms = firstDay["total_search_terms"] as? Int, + let hiddenSearchTerms = firstDay["encrypted_search_terms"] as? Int, + let otherSearchTerms = firstDay["other_search_terms"] as? Int, + let searchTermsDict = firstDay["search_terms"] as? [[String: AnyObject]] + else { + return nil + } + + let searchTerms: [SearchTerm] = searchTermsDict.compactMap { + guard let term = $0["term"] as? String, let views = $0["views"] as? Int else { + return nil + } + + return SearchTerm(term: term, viewsCount: views) + } + + + self.periodEndDate = date + self.period = period + self.totalSearchTermsCount = totalSearchTerms + self.hiddenSearchTermsCount = hiddenSearchTerms + self.otherSearchTermsCount = otherSearchTerms + self.searchTerms = searchTerms + } + +} + +public struct SearchTerm { + let term: String + let viewsCount: Int +} From 674f8a27ec797dd285914e91f86388d30c46c8dd Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Mon, 18 Feb 2019 19:53:33 +0100 Subject: [PATCH 03/27] add mocked data for stats search term query --- WordPressKit.xcodeproj/project.pbxproj | 4 ++ .../Mock Data/stats-search-term-result.json | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 WordPressKitTests/Mock Data/stats-search-term-result.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 11abe34a..cdcb2c98 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 40247DFA2120D8E100AE1C3C /* AutomatedTransferService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */; }; 40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */; }; 404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C4221B30400060250C /* SearchTermStatsType.swift */; }; + 404057C7221B36070060250C /* stats-search-term-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057C6221B36070060250C /* stats-search-term-result.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -492,6 +493,7 @@ 40247DF92120D8E100AE1C3C /* AutomatedTransferService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferService.swift; sourceTree = ""; }; 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferStatus.swift; sourceTree = ""; }; 404057C4221B30400060250C /* SearchTermStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTermStatsType.swift; sourceTree = ""; }; + 404057C6221B36070060250C /* stats-search-term-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-search-term-result.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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -1511,6 +1513,7 @@ isa = PBXGroup; children = ( 40F9880D221ACFB400B7B369 /* stats-streak-result.json */, + 404057C6221B36070060250C /* stats-search-term-result.json */, 826016F61F9FAF6300533B6C /* activity-log-auth-failure.json */, 826016F81F9FAF6300533B6C /* activity-log-bad-json-failure.json */, 826016F51F9FAF6300533B6C /* activity-log-success-1.json */, @@ -1976,6 +1979,7 @@ 93F50A471F227F3600B5BEBA /* xmlrpc-response-getprofile.xml in Resources */, 7403A2F51EF06FEB00DED7DC /* me-settings-bad-json-failure.json in Resources */, 9AB6D64E218731AB0008F274 /* post-revisions-mapping-success.json in Resources */, + 404057C7221B36070060250C /* stats-search-term-result.json in Resources */, 7403A2F41EF06FEB00DED7DC /* me-settings-auth-failure.json in Resources */, 930999581F16598A00F006A1 /* get-single-theme-v1.1.json in Resources */, 74B335E61F06F6E90053A184 /* WordPressComRestApiMedia.json in Resources */, diff --git a/WordPressKitTests/Mock Data/stats-search-term-result.json b/WordPressKitTests/Mock Data/stats-search-term-result.json new file mode 100644 index 00000000..d5919268 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-search-term-result.json @@ -0,0 +1,49 @@ +{ + "date": "2019-02-17", + "period": "week", + "days": { + "2019-02-11": { + "search_terms": [ + { + "term": "wordpress", + "views": 16 + }, + { + "term": "wordpress.com", + "views": 5 + }, + { + "term": "undefined", + "views": 5 + }, + { + "term": "i want to read blogs", + "views": 3 + }, + { + "term": "wordpress com twenty fourteen", + "views": 3 + }, + { + "term": "where to read blogs", + "views": 3 + }, + { + "term": "pensamiento la informacion por medios digitales", + "views": 3 + }, + { + "term": "read blogs", + "views": 3 + }, + { + "term": "wp me", + "views": 2 + } + ], + "encrypted_search_terms": 634, + "other_search_terms": 190, + "total_search_terms": 867 + } + } +} From 61bc6f7c6f2aeec60888605ace103276e315a3e0 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 01:23:46 +0100 Subject: [PATCH 04/27] add support for fetching Authors stats --- WordPressKit.xcodeproj/project.pbxproj | 8 + .../Time-based data/AuthorsStatsType.swift | 84 ++++ .../Mock Data/stats-top-authors.json | 455 ++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 61 ++- 4 files changed, 607 insertions(+), 1 deletion(-) create mode 100644 WordPressKit/Time-based data/AuthorsStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-top-authors.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index cdcb2c98..49bdd1ab 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 40247DFC2120E69600AE1C3C /* AutomatedTransferStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */; }; 404057C5221B30400060250C /* SearchTermStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C4221B30400060250C /* SearchTermStatsType.swift */; }; 404057C7221B36070060250C /* stats-search-term-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057C6221B36070060250C /* stats-search-term-result.json */; }; + 404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C8221B789B0060250C /* AuthorsStatsType.swift */; }; + 404057CB221B80BC0060250C /* stats-top-authors.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CA221B80BC0060250C /* stats-top-authors.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -494,6 +496,8 @@ 40247DFB2120E69600AE1C3C /* AutomatedTransferStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomatedTransferStatus.swift; sourceTree = ""; }; 404057C4221B30400060250C /* SearchTermStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTermStatsType.swift; sourceTree = ""; }; 404057C6221B36070060250C /* stats-search-term-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-search-term-result.json"; sourceTree = ""; }; + 404057C8221B789B0060250C /* AuthorsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsStatsType.swift; sourceTree = ""; }; + 404057CA221B80BC0060250C /* stats-top-authors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-top-authors.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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -995,6 +999,7 @@ isa = PBXGroup; children = ( 404057C4221B30400060250C /* SearchTermStatsType.swift */, + 404057C8221B789B0060250C /* AuthorsStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1512,6 +1517,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 404057CA221B80BC0060250C /* stats-top-authors.json */, 40F9880D221ACFB400B7B369 /* stats-streak-result.json */, 404057C6221B36070060250C /* stats-search-term-result.json */, 826016F61F9FAF6300533B6C /* activity-log-auth-failure.json */, @@ -2057,6 +2063,7 @@ 74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */, D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */, 740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */, + 404057CB221B80BC0060250C /* stats-top-authors.json in Resources */, 93AC8ED51ED32FD000900F5A /* stats-v1.1-tags-categories-views-day.json in Resources */, 17BF9A7520C7E18200BF57D2 /* reader-site-search-success-no-icon.json in Resources */, 828A2402201B6B8A004F6859 /* activity-rewind-status-restore-in-progress.json in Resources */, @@ -2296,6 +2303,7 @@ 742362E31F1025B400BD0A7F /* RemoteMenuLocation.m in Sources */, 82FFBF561F460DD400F4573F /* BlogJetpackSettingsServiceRemote.swift in Sources */, 9368C7B81EC630270092CE8E /* StatsStreakItem.m in Sources */, + 404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */, 74BA04F61F06DC0A00ED5CD8 /* CommentServiceRemoteXMLRPC.m in Sources */, 7430C9A81F1927180051B8E6 /* ReaderTopicServiceRemote.m in Sources */, 17CE77F120C6EB41001DEA5A /* ReaderFeed.swift in Sources */, diff --git a/WordPressKit/Time-based data/AuthorsStatsType.swift b/WordPressKit/Time-based data/AuthorsStatsType.swift new file mode 100644 index 00000000..c2755ab1 --- /dev/null +++ b/WordPressKit/Time-based data/AuthorsStatsType.swift @@ -0,0 +1,84 @@ +struct AuthorsStatsType { + let period: StatsPeriodUnit + let periodEndDate: Date + + let topAuthors: [StatsTopAuthor] +} + +struct StatsTopAuthor { + let name: String + let iconURL: URL? + let viewsCount: Int + let posts: [StatsTopPost] + + init?(jsonDictionary: [String: AnyObject]) { + guard + let name = jsonDictionary["name"] as? String, + let views = jsonDictionary["views"] as? Int, + let avatar = jsonDictionary["avatar"] as? String, + let posts = jsonDictionary["posts"] as? [[String: AnyObject]] + else { + return nil + } + + let url: URL? + if var components = URLComponents(string: avatar) { + components.query = "d=mm&s=60" + url = try? components.asURL() + } else { + url = nil + } + + let mappedPosts = posts.compactMap { StatsTopPost(jsonDictionary: $0) } + + self.name = name + self.viewsCount = views + self.iconURL = url + self.posts = mappedPosts + + } +} + +struct StatsTopPost { + let title: String + let postID: Int + let postURL: URL? + let viewsCount: Int + + init?(jsonDictionary: [String: AnyObject]) { + guard + let title = jsonDictionary["title"] as? String, + let postID = jsonDictionary["id"] as? Int, + let viewsCount = jsonDictionary["views"] as? Int, + let postURL = jsonDictionary["url"] as? String + else { + return nil + } + + self.title = title + self.postID = postID + self.viewsCount = viewsCount + self.postURL = URL(string: postURL) + } +} + +extension AuthorsStatsType: TimeStatsProtocol { + static var pathComponent: String { + return "stats/top-authors/" + } + + init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let days = jsonDictionary["days"] as? [String: AnyObject], + let firstKey = days.keys.first, + let firstDay = days[firstKey] as? [String: AnyObject], + let authors = firstDay["authors"] as? [[String: AnyObject]] + else { + return nil + } + + self.period = period + self.periodEndDate = date + self.topAuthors = authors.compactMap { StatsTopAuthor(jsonDictionary: $0) } + } +} diff --git a/WordPressKitTests/Mock Data/stats-top-authors.json b/WordPressKitTests/Mock Data/stats-top-authors.json new file mode 100644 index 00000000..5d0e9a24 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-top-authors.json @@ -0,0 +1,455 @@ +{ + "date": "2018-12-31", + "days": { + "2018-01-01": { + "authors": [ + { + "name": "George Hotelling", + "avatar": "https://0.gravatar.com/avatar/9c9aa771f98b781c3fea988f6d925c9f?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 57, + "posts": [ + { + "id": 132, + "title": "Josepha's Prospect ", + "url": "http://bagomattic.wordpress.com/2016/09/20/josephas-prospect/", + "views": 7, + "video": false + }, + { + "id": 122, + "title": "Stef's Magic Box", + "url": "http://bagomattic.wordpress.com/2016/09/18/stefs-magic-box/", + "views": 7, + "video": false + }, + { + "id": 138, + "title": "Kelly's Mini Prospect ", + "url": "http://bagomattic.wordpress.com/2016/09/21/kellys-mini-prospect/", + "views": 5, + "video": false + }, + { + "id": 114, + "title": "James' Alcatraz ", + "url": "http://bagomattic.wordpress.com/2016/09/17/james-alcatraz/", + "views": 5, + "video": false + }, + { + "id": 100, + "title": "Rodrigo's Prospect", + "url": "http://bagomattic.wordpress.com/2016/09/17/rodrigos-prospect/", + "views": 5, + "video": false + }, + { + "id": 124, + "title": "Sirin's Prospect", + "url": "http://bagomattic.wordpress.com/2016/09/19/sirins-prospect/", + "views": 5, + "video": false + }, + { + "id": 135, + "title": "Chrissie's Prospect", + "url": "http://bagomattic.wordpress.com/2016/09/21/chrissies-prospect/", + "views": 4, + "video": false + }, + { + "id": 119, + "title": "Michelle's Classic Backpack ", + "url": "http://bagomattic.wordpress.com/2016/09/17/michelles-classic-backpack/", + "views": 3, + "video": false + }, + { + "id": 94, + "title": "Hari's Alcatraz", + "url": "http://bagomattic.wordpress.com/2016/09/16/haris-alcatraz/", + "views": 3, + "video": false + }, + { + "id": 109, + "title": "Ingrid's Messenger ", + "url": "http://bagomattic.wordpress.com/2016/09/17/ingrids-messenger/", + "views": 2, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "g13gblog.wordpress.com", + "blog_url": "http://g13gblog.wordpress.com", + "blog_id": 153485459, + "site_id": 153485459, + "blog_title": "g13g", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Steph Yiu", + "avatar": "https://0.gravatar.com/avatar/c5f0f64e51d3117184032db715963b39?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 28, + "posts": [ + { + "id": 9, + "title": "Not quite newdash blue", + "url": "http://bagomattic.wordpress.com/2014/09/15/not-quite-newdash-blue/", + "views": 5, + "video": false + }, + { + "id": 18, + "title": "Incognito", + "url": "http://bagomattic.wordpress.com/2014/09/16/incognito/", + "views": 4, + "video": false + }, + { + "id": 51, + "title": "Excellent beer bottle opener", + "url": "http://bagomattic.wordpress.com/2014/09/23/excellent-beer-bottle-opener/", + "views": 4, + "video": false + }, + { + "id": 42, + "title": "Out and about", + "url": "http://bagomattic.wordpress.com/2014/09/18/out-and-about/", + "views": 3, + "video": false + }, + { + "id": 39, + "title": "It's solid blue", + "url": "http://bagomattic.wordpress.com/2014/09/18/its-solid-blue/", + "views": 2, + "video": false + }, + { + "id": 73, + "title": "Jose's bag", + "url": "http://bagomattic.wordpress.com/2015/05/13/joses-bag/", + "views": 2, + "video": false + }, + { + "id": 36, + "title": "Tweedledee", + "url": "http://bagomattic.wordpress.com/2014/09/18/tweedledee/", + "views": 2, + "video": false + }, + { + "id": 5, + "title": "Orange is the new black", + "url": "http://bagomattic.wordpress.com/2014/09/15/orange-is-the-new-black/", + "views": 1, + "video": false + }, + { + "id": 15, + "title": "It ain't easy being green", + "url": "http://bagomattic.wordpress.com/2014/09/15/it-aint-easy-being-green/", + "views": 1, + "video": false + }, + { + "id": 69, + "title": "Michael's bag", + "url": "http://bagomattic.wordpress.com/2015/05/13/michaels-bag/", + "views": 1, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "automattic.wordpress.com", + "blog_url": "http://automattic.wordpress.com", + "blog_id": 54117, + "site_id": 54117, + "blog_title": "Automattic", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Eric Gunawan", + "avatar": "https://1.gravatar.com/avatar/a2e61952c5e6787763903f88e180ce2e?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 25, + "posts": [ + { + "id": 254, + "title": "Eric's Indonesian-Themed Prospect", + "url": "http://bagomattic.wordpress.com/2018/03/16/erics-indonesian-themed-prospect/", + "views": 23, + "video": false + }, + { + "id": 255, + "title": "EricProspect1", + "url": "http://bagomattic.wordpress.com/2018/03/16/erics-indonesian-themed-prospect/ericprospect1/", + "views": 1, + "video": false + }, + { + "id": 256, + "title": "EricProspect2", + "url": "http://bagomattic.wordpress.com/2018/03/16/erics-indonesian-themed-prospect/ericprospect2/", + "views": 1, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "erricgunawan.com", + "blog_url": "http://erricgunawan.com/blog", + "blog_id": 31459430, + "site_id": 31459430, + "blog_title": "Eric Gunawan in Blue", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Cesar Tardaguila", + "avatar": "https://2.gravatar.com/avatar/b371b7de1e58a5dcc3fc3aa236784081?s=64&d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 20, + "posts": [ + { + "id": 246, + "title": "Cesar's new division pack", + "url": "http://bagomattic.wordpress.com/2018/03/13/cesars-new-division-pack/", + "views": 20, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "ctarda.com", + "blog_url": "http://ctarda.com", + "blog_id": 88556049, + "site_id": 88556049, + "blog_title": "The Amazing Adventures of a Sexy Software Engineer", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Pete Schiebel", + "avatar": "https://0.gravatar.com/avatar/6b764339c0e5580d28a45c3476bdd9d9?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 16, + "posts": [ + { + "id": 203, + "title": "Pete's Prospect", + "url": "http://bagomattic.wordpress.com/2017/08/21/petes-prospect/", + "views": 13, + "video": false + }, + { + "id": 205, + "title": "IMG_3437", + "url": "http://bagomattic.wordpress.com/2017/08/21/petes-prospect/img_3437/", + "views": 2, + "video": false + }, + { + "id": 204, + "title": "IMG_8124", + "url": "http://bagomattic.wordpress.com/2017/08/21/petes-prospect/img_8124/", + "views": 1, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "peterschiebelblog.wordpress.com", + "blog_url": "http://peterschiebelblog.wordpress.com", + "blog_id": 136017937, + "site_id": 136017937, + "blog_title": "Paul's Painting & Pressure Washing", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Jaco", + "avatar": "https://0.gravatar.com/avatar/0b79f6ec1d8ca34b3307f17845791297?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 15, + "posts": [ + { + "id": 352, + "title": "Jaco's Aer Fit Pack 2", + "url": "http://bagomattic.wordpress.com/2018/11/29/jacos-aer-fit-pack-2/", + "views": 6, + "video": false + }, + { + "id": 353, + "title": "Jaco's Aer Fit Pack 2", + "url": "http://bagomattic.wordpress.com/2018/11/29/jacos-aer-fit-pack-2/img_20181129_140741/", + "views": 3, + "video": false + }, + { + "id": 354, + "title": "Jaco's Aer Fit Pack 2", + "url": "http://bagomattic.wordpress.com/2018/11/29/jacos-aer-fit-pack-2/img_20181129_140819/", + "views": 2, + "video": false + }, + { + "id": 355, + "title": "Jaco's Aer Fit Pack 2", + "url": "http://bagomattic.wordpress.com/2018/11/29/jacos-aer-fit-pack-2/img_20181129_140918/", + "views": 2, + "video": false + }, + { + "id": 361, + "title": "Field Tested", + "url": "http://bagomattic.wordpress.com/2018/11/29/jacos-aer-fit-pack-2/field-tested-aer-fit-bag/", + "views": 2, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "domainsuniversity.wordpress.com", + "blog_url": "http://domainsuniversity.wordpress.com", + "blog_id": 102996235, + "site_id": 102996235, + "blog_title": "Domains University", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Ivan Ottinger", + "avatar": "https://1.gravatar.com/avatar/dcf30f3f3a3a09d40aa7c76e5bb35f3c?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 15, + "posts": [ + { + "id": 240, + "title": "Ivan's new Timbuk2", + "url": "http://bagomattic.wordpress.com/2018/02/21/ivans-new-timbuk2/", + "views": 15, + "video": false + } + ], + "follow_data": false + }, + { + "name": "Paulo", + "avatar": "https://1.gravatar.com/avatar/da029fa4d07f05dace95562631d6e66c?s=64&d=https%3A%2F%2F1.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 14, + "posts": [ + { + "id": 284, + "title": "Paulo's Grey Aer Fit Pack 2", + "url": "http://bagomattic.wordpress.com/2018/04/07/paulos-grey-aer-fit-pack-2/", + "views": 14, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "peaeb.wordpress.com", + "blog_url": "https://peaeb.me", + "blog_id": 125791165, + "site_id": 125791165, + "blog_title": "Everything Builds", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Khyati Gala", + "avatar": "https://0.gravatar.com/avatar/34d3d0f93dda307e270fc7515d50914a?s=64&d=https%3A%2F%2F0.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 14, + "posts": [ + { + "id": 215, + "title": "Khyati's Timbuk2 bagpack", + "url": "http://bagomattic.wordpress.com/2017/11/02/khyatis-timbuk2-bagpack/", + "views": 14, + "video": false + } + ], + "follow_data": { + "params": { + "stat-source": "stats_author", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "wptest451.wordpress.com", + "blog_url": "https://mywptest.blog", + "blog_id": 131325209, + "site_id": 131325209, + "blog_title": "My WordPress Test site", + "is_following": false + }, + "type": "follow" + } + }, + { + "name": "Liz Swafford", + "avatar": "https://2.gravatar.com/avatar/84cd07b56d9cf864ce9f818720ef1062?s=64&d=https%3A%2F%2F2.gravatar.com%2Favatar%2Fad516503a11cd5ca435acc9bb6523536%3Fs%3D64&r=G", + "views": 14, + "posts": [ + { + "id": 229, + "title": "Liz's Alcatraz in Grey and Black", + "url": "http://bagomattic.wordpress.com/2018/02/16/alcatraz-in-grey-and-black/", + "views": 14, + "video": false + } + ], + "follow_data": false + } + ], + "other_views": 207 + } + }, + "period": "year" +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index b7d48f2e..5e5d1e56 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -8,11 +8,16 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let siteID = 321 - let getStreakMockFilename = "stats-streak-result.json" + let getStreakMockFilename = "stats-streak-result.json" + let getSearchDataFilename = "stats-search-term-result.json" + let getAuthorsDataFilename = "stats-top-authors.json" // MARK: - Properties var siteStreakEndpoint: String { return "sites/\(siteID)/stats/streak" } + var siteSearchDataEndpoint: String { return "sites/\(siteID)/stats/search-terms/" } + var siteAuthorsDataEndpoint: String { return "sites/\(siteID)/stats/top-authors/" } + var remote: StatsServiceRemoteV2! // MARK: - Overridden Methods @@ -53,4 +58,58 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testFetchSearchData() { + let expect = expectation(description: "It should return search data for a week") + + stubRemoteResponse(siteSearchDataEndpoint, filename: getSearchDataFilename, contentType: .ApplicationJSON) + + remote.getData(for: .week, endingOn: Date()) { (searchTerms: SearchTermStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(searchTerms) + + XCTAssertEqual(searchTerms!.hiddenSearchTermsCount, 634) + XCTAssertEqual(searchTerms!.otherSearchTermsCount, 190) + XCTAssertEqual(searchTerms!.totalSearchTermsCount, 867) + + XCTAssertEqual(searchTerms?.searchTerms.count, 9) + XCTAssertEqual(searchTerms?.searchTerms.first!.term, "wordpress") + XCTAssertEqual(searchTerms?.searchTerms.first!.viewsCount, 16) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testTopAuthors() { + let expect = expectation(description: "It should return authors data for a year") + + stubRemoteResponse(siteAuthorsDataEndpoint, filename: getAuthorsDataFilename, contentType: .ApplicationJSON) + + let dec31 = DateComponents(year: 2018, month: 12, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: dec31)! + + + remote.getData(for: .year, endingOn: date) { (topAuthors: AuthorsStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(topAuthors) + + XCTAssertEqual(topAuthors?.topAuthors.count, 10) + + XCTAssertEqual(topAuthors?.topAuthors.first!.viewsCount, 57) + XCTAssertEqual(topAuthors?.topAuthors.first!.name, "George Hotelling") + + XCTAssertEqual(topAuthors?.topAuthors.first!.posts.count, 10) + XCTAssertEqual(topAuthors?.topAuthors.first!.posts.first!.postID, 132) + XCTAssertEqual(topAuthors?.topAuthors.first!.posts.first!.viewsCount, 7) + XCTAssertEqual(topAuthors?.topAuthors.first!.posts.first!.title, "Josepha's Prospect ") + XCTAssertEqual(topAuthors?.topAuthors.first!.posts.first!.postURL, URL(string: "http://bagomattic.wordpress.com/2016/09/20/josephas-prospect/")) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + } } From c44a11dfa28f1db389e221ef6ed566c6bf60df3d Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 15:28:22 +0100 Subject: [PATCH 05/27] clean up time-based stats data parsing --- WordPressKit/StatsServiceRemoteV2.swift | 6 +- .../Time-based data/AuthorsStatsType.swift | 73 ++++++++++--------- .../Time-based data/SearchTermStatsType.swift | 39 +++++----- 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index de5094be..85ca598a 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -71,12 +71,14 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in guard - let jsonResponse = response as? [String: AnyObject], let dateString = response["date"] as? String, let date = self.periodDataQueryDateFormatter.date(from: dateString), let periodString = response["period"] as? String, let parsedPeriod = StatsPeriodUnit(string: periodString), - let timestats = TimeStatsType(date: date, period: parsedPeriod, jsonDictionary: jsonResponse) + let days = response["days"] as? [String: AnyObject], + let firstKey = days.keys.first, + let firstDay = days[firstKey] as? [String: AnyObject], + let timestats = TimeStatsType(date: date, period: parsedPeriod, jsonDictionary: firstDay) else { completion(nil, ResponseError.decodingFailure) return diff --git a/WordPressKit/Time-based data/AuthorsStatsType.swift b/WordPressKit/Time-based data/AuthorsStatsType.swift index c2755ab1..3fcdb60c 100644 --- a/WordPressKit/Time-based data/AuthorsStatsType.swift +++ b/WordPressKit/Time-based data/AuthorsStatsType.swift @@ -1,16 +1,44 @@ -struct AuthorsStatsType { - let period: StatsPeriodUnit - let periodEndDate: Date +public struct AuthorsStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date - let topAuthors: [StatsTopAuthor] + public let topAuthors: [StatsTopAuthor] } -struct StatsTopAuthor { - let name: String - let iconURL: URL? - let viewsCount: Int - let posts: [StatsTopPost] +public struct StatsTopAuthor { + public let name: String + public let iconURL: URL? + public let viewsCount: Int + public let posts: [StatsTopPost] +} + + +public struct StatsTopPost { + public let title: String + public let postID: Int + public let postURL: URL? + public let viewsCount: Int +} + +extension AuthorsStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/top-authors/" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let authors = jsonDictionary["authors"] as? [[String: AnyObject]] + else { + return nil + } + + self.period = period + self.periodEndDate = date + self.topAuthors = authors.compactMap { StatsTopAuthor(jsonDictionary: $0) } + } +} +extension StatsTopAuthor { init?(jsonDictionary: [String: AnyObject]) { guard let name = jsonDictionary["name"] as? String, @@ -39,12 +67,7 @@ struct StatsTopAuthor { } } -struct StatsTopPost { - let title: String - let postID: Int - let postURL: URL? - let viewsCount: Int - +extension StatsTopPost { init?(jsonDictionary: [String: AnyObject]) { guard let title = jsonDictionary["title"] as? String, @@ -62,23 +85,3 @@ struct StatsTopPost { } } -extension AuthorsStatsType: TimeStatsProtocol { - static var pathComponent: String { - return "stats/top-authors/" - } - - init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { - guard - let days = jsonDictionary["days"] as? [String: AnyObject], - let firstKey = days.keys.first, - let firstDay = days[firstKey] as? [String: AnyObject], - let authors = firstDay["authors"] as? [[String: AnyObject]] - else { - return nil - } - - self.period = period - self.periodEndDate = date - self.topAuthors = authors.compactMap { StatsTopAuthor(jsonDictionary: $0) } - } -} diff --git a/WordPressKit/Time-based data/SearchTermStatsType.swift b/WordPressKit/Time-based data/SearchTermStatsType.swift index 44d0ede0..b3238509 100644 --- a/WordPressKit/Time-based data/SearchTermStatsType.swift +++ b/WordPressKit/Time-based data/SearchTermStatsType.swift @@ -1,28 +1,29 @@ -struct SearchTermStatsType { - let period: StatsPeriodUnit - let periodEndDate: Date - - let totalSearchTermsCount: Int - let hiddenSearchTermsCount: Int - let otherSearchTermsCount: Int - let searchTerms: [SearchTerm] +public struct SearchTermStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalSearchTermsCount: Int + public let hiddenSearchTermsCount: Int + public let otherSearchTermsCount: Int + public let searchTerms: [SearchTerm] } +public struct SearchTerm { + public let term: String + public let viewsCount: Int +} extension SearchTermStatsType: TimeStatsProtocol { - static var pathComponent: String { + public static var pathComponent: String { return "stats/search-terms" } - init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { guard - let days = jsonDictionary["days"] as? [String: AnyObject], - let firstKey = days.keys.first, - let firstDay = days[firstKey] as? [String: AnyObject], - let totalSearchTerms = firstDay["total_search_terms"] as? Int, - let hiddenSearchTerms = firstDay["encrypted_search_terms"] as? Int, - let otherSearchTerms = firstDay["other_search_terms"] as? Int, - let searchTermsDict = firstDay["search_terms"] as? [[String: AnyObject]] + let totalSearchTerms = jsonDictionary["total_search_terms"] as? Int, + let hiddenSearchTerms = jsonDictionary["encrypted_search_terms"] as? Int, + let otherSearchTerms = jsonDictionary["other_search_terms"] as? Int, + let searchTermsDict = jsonDictionary["search_terms"] as? [[String: AnyObject]] else { return nil } @@ -46,7 +47,3 @@ extension SearchTermStatsType: TimeStatsProtocol { } -public struct SearchTerm { - let term: String - let viewsCount: Int -} From 77c8dfb39e56c72c20280a6fba1db24bc1958274 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 15:28:50 +0100 Subject: [PATCH 06/27] add support for fetching vidoes stats --- WordPressKit.xcodeproj/project.pbxproj | 8 +++ .../Time-based data/VideosStatsType.swift | 58 +++++++++++++++ .../Mock Data/stats-videos-data.json | 72 +++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 35 +++++++++ 4 files changed, 173 insertions(+) create mode 100644 WordPressKit/Time-based data/VideosStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-videos-data.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 49bdd1ab..ca2496c7 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 404057C7221B36070060250C /* stats-search-term-result.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057C6221B36070060250C /* stats-search-term-result.json */; }; 404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057C8221B789B0060250C /* AuthorsStatsType.swift */; }; 404057CB221B80BC0060250C /* stats-top-authors.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CA221B80BC0060250C /* stats-top-authors.json */; }; + 404057CE221C38130060250C /* VideosStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057CD221C38130060250C /* VideosStatsType.swift */; }; + 404057D0221C46790060250C /* stats-videos-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CF221C46780060250C /* stats-videos-data.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -498,6 +500,8 @@ 404057C6221B36070060250C /* stats-search-term-result.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-search-term-result.json"; sourceTree = ""; }; 404057C8221B789B0060250C /* AuthorsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorsStatsType.swift; sourceTree = ""; }; 404057CA221B80BC0060250C /* stats-top-authors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-top-authors.json"; sourceTree = ""; }; + 404057CD221C38130060250C /* VideosStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosStatsType.swift; sourceTree = ""; }; + 404057CF221C46780060250C /* stats-videos-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-videos-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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -1000,6 +1004,7 @@ children = ( 404057C4221B30400060250C /* SearchTermStatsType.swift */, 404057C8221B789B0060250C /* AuthorsStatsType.swift */, + 404057CD221C38130060250C /* VideosStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1517,6 +1522,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 404057CF221C46780060250C /* stats-videos-data.json */, 404057CA221B80BC0060250C /* stats-top-authors.json */, 40F9880D221ACFB400B7B369 /* stats-streak-result.json */, 404057C6221B36070060250C /* stats-search-term-result.json */, @@ -2041,6 +2047,7 @@ 74B335E01F06F6290053A184 /* WordPressComRestApiFailInvalidInput.json in Resources */, FFE247C320C9D749002DF3A2 /* reader-site-search-blog-id-fallback.json in Resources */, 93BD27651EE73442002BB00B /* me-sites-visibility-success.json in Resources */, + 404057D0221C46790060250C /* stats-videos-data.json in Resources */, 93AC8ED61ED32FD000900F5A /* stats-v1.1-top-posts-day-exception.json in Resources */, E1787DB0200E564B004CB3AF /* timezones.json in Resources */, 93BD275E1EE73442002BB00B /* me-bad-json-failure.json in Resources */, @@ -2317,6 +2324,7 @@ 40AB1ADA200FED25009B533D /* PluginDirectoryFeedPage.swift in Sources */, 436D56352118D85800CEAA33 /* Country.swift in Sources */, 74A44DCB1F13C533006CD8F4 /* NotificationSettingsServiceRemote.swift in Sources */, + 404057CE221C38130060250C /* VideosStatsType.swift in Sources */, E182BF6A1FD961810001D850 /* Endpoint.swift in Sources */, 9AF4F2FF2183346B00570E4B /* RemoteRevision.swift in Sources */, 74BA04F41F06DC0A00ED5CD8 /* CommentServiceRemoteREST.m in Sources */, diff --git a/WordPressKit/Time-based data/VideosStatsType.swift b/WordPressKit/Time-based data/VideosStatsType.swift new file mode 100644 index 00000000..494f3d91 --- /dev/null +++ b/WordPressKit/Time-based data/VideosStatsType.swift @@ -0,0 +1,58 @@ +public struct VideoStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalPlaysCount: Int + public let otherPlayCount: Int + public let videos: [StatsVideo] +} + +public struct StatsVideo { + let postID: Int + let title: String + let playsCount: Int + let videoURL: URL? + + +} + +extension VideoStatsType: TimeStatsProtocol { + + public static var pathComponent: String { + return "stats/video-plays" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let totalPlayCount = jsonDictionary["total_plays"] as? Int, + let otherPlays = jsonDictionary["other_plays"] as? Int, + let videos = jsonDictionary["plays"] as? [[String: AnyObject]] + else { + return nil + } + + self.period = period + self.periodEndDate = date + self.totalPlaysCount = totalPlayCount + self.otherPlayCount = otherPlays + self.videos = videos.compactMap { StatsVideo(jsonDictionary: $0) } + } +} + +extension StatsVideo { + init?(jsonDictionary: [String: AnyObject]) { + guard + let postID = jsonDictionary["post_id"] as? Int, + let title = jsonDictionary["title"] as? String, + let playsCount = jsonDictionary["plays"] as? Int, + let url = jsonDictionary["url"] as? String + else { + return nil + } + + self.postID = postID + self.title = title + self.playsCount = playsCount + self.videoURL = URL(string: url) + } +} diff --git a/WordPressKitTests/Mock Data/stats-videos-data.json b/WordPressKitTests/Mock Data/stats-videos-data.json new file mode 100644 index 00000000..ed7d049d --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-videos-data.json @@ -0,0 +1,72 @@ +{ + "date": "2019-12-31", + "period": "year", + "days": { + "2019-01-01": { + "plays": [ + { + "post_id": 9001, + "url": "http://wordpress.com/random_nonexisting_videos_1.mp4", + "title": "you won't believe what's number two", + "plays": 7774 + }, + { + "post_id": 9002, + "url": "http://wordpress.com/random_nonexisting_videos_2.mp4", + "title": "10 wierd tricks for better tests coverage!!!!!!", + "plays": 3416 + }, + { + "post_id": 9003, + "url": "http://wordpress.com/random_nonexisting_videos_3.mp4", + "title": "is this anything", + "plays": 1015 + }, + { + "post_id": 9004, + "url": "http://wordpress.com/random_nonexisting_videos_4.mp4", + "title": "did this get funny yet", + "plays": 450 + }, + { + "post_id": 9005, + "url": "http://wordpress.com/random_nonexisting_videos_5.mp4", + "title": "please send help", + "plays": 243 + }, + { + "post_id": 9006, + "url": "http://wordpress.com/random_nonexisting_videos_6.mp4", + "title": "what's the deal with airplane food, am i right", + "plays": 186 + }, + { + "post_id": 9007, + "url": "http://wordpress.com/random_nonexisting_videos_7.mp4", + "title": "hey i just met you", + "plays": 154 + }, + { + "post_id": 9008, + "url": "http://wordpress.com/random_nonexisting_videos_8.mp4", + "title": "and this is crazy", + "plays": 138 + }, + { + "post_id": 9009, + "url": "http://wordpress.com/random_nonexisting_videos_9.mp4", + "title": "but here's my number", + "plays": 126 + }, + { + "post_id": 9010, + "url": "http://wordpress.com/random_nonexisting_videos_10.mp4", + "title": "so call me maybe?", + "plays": 97 + } + ], + "other_plays": 62, + "total_plays": 13661 + } + } +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 5e5d1e56..abfe4dcf 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -11,12 +11,14 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getStreakMockFilename = "stats-streak-result.json" let getSearchDataFilename = "stats-search-term-result.json" let getAuthorsDataFilename = "stats-top-authors.json" + let getVideosMockFilename = "stats-videos-data.json" // MARK: - Properties var siteStreakEndpoint: String { return "sites/\(siteID)/stats/streak" } var siteSearchDataEndpoint: String { return "sites/\(siteID)/stats/search-terms/" } var siteAuthorsDataEndpoint: String { return "sites/\(siteID)/stats/top-authors/" } + var siteVideosDataEndpoint: String { return "sites/\(siteID)/stats/video-plays/" } var remote: StatsServiceRemoteV2! @@ -112,4 +114,37 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testVideos() { + let expect = expectation(description: "It should return video data for a year") + + stubRemoteResponse(siteVideosDataEndpoint, filename: getVideosMockFilename, contentType: .ApplicationJSON) + + let dec31 = DateComponents(year: 2019, month: 12, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: dec31)! + + + remote.getData(for: .year, endingOn: date) { (videos: VideoStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(videos) + + XCTAssertEqual(videos?.totalPlaysCount, 13661) + XCTAssertEqual(videos?.otherPlayCount, 62) + + XCTAssertEqual(videos?.videos.count, 10) + + XCTAssertEqual(videos?.videos.first!.playsCount, 7774) + XCTAssertEqual(videos?.videos.first!.title, "you won't believe what's number two") + XCTAssertEqual(videos?.videos.first!.postID, 9001) + + XCTAssertEqual(videos?.videos.last!.playsCount, 97) + XCTAssertEqual(videos?.videos.last!.postID, 9010) + XCTAssertEqual(videos?.videos.last!.title, "so call me maybe?") + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + } } From 89b7d0ffda32ddfdc7410e5c5c3fbd0e68ede609 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 17:24:08 +0100 Subject: [PATCH 07/27] add support for fetching countries --- WordPressKit.xcodeproj/project.pbxproj | 8 ++ WordPressKit/StatsServiceRemoteV2.swift | 41 +++++-- .../Time-based data/AuthorsStatsType.swift | 3 +- .../Time-based data/CountryStatsType.swift | 66 ++++++++++ .../Time-based data/SearchTermStatsType.swift | 9 +- .../Time-based data/VideosStatsType.swift | 7 +- .../Mock Data/stats-countries-data.json | 113 ++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 36 ++++++ 8 files changed, 267 insertions(+), 16 deletions(-) create mode 100644 WordPressKit/Time-based data/CountryStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-countries-data.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index ca2496c7..91ff7115 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ 404057CB221B80BC0060250C /* stats-top-authors.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CA221B80BC0060250C /* stats-top-authors.json */; }; 404057CE221C38130060250C /* VideosStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057CD221C38130060250C /* VideosStatsType.swift */; }; 404057D0221C46790060250C /* stats-videos-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CF221C46780060250C /* stats-videos-data.json */; }; + 404057D2221C56AB0060250C /* CountryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057D1221C56AB0060250C /* CountryStatsType.swift */; }; + 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057D3221C5FC30060250C /* stats-countries-data.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -502,6 +504,8 @@ 404057CA221B80BC0060250C /* stats-top-authors.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-top-authors.json"; sourceTree = ""; }; 404057CD221C38130060250C /* VideosStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosStatsType.swift; sourceTree = ""; }; 404057CF221C46780060250C /* stats-videos-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-videos-data.json"; sourceTree = ""; }; + 404057D1221C56AB0060250C /* CountryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryStatsType.swift; sourceTree = ""; }; + 404057D3221C5FC30060250C /* stats-countries-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-countries-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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -1005,6 +1009,7 @@ 404057C4221B30400060250C /* SearchTermStatsType.swift */, 404057C8221B789B0060250C /* AuthorsStatsType.swift */, 404057CD221C38130060250C /* VideosStatsType.swift */, + 404057D1221C56AB0060250C /* CountryStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1522,6 +1527,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 404057D3221C5FC30060250C /* stats-countries-data.json */, 404057CF221C46780060250C /* stats-videos-data.json */, 404057CA221B80BC0060250C /* stats-top-authors.json */, 40F9880D221ACFB400B7B369 /* stats-streak-result.json */, @@ -2002,6 +2008,7 @@ 93AC8ED91ED32FD000900F5A /* stats-v1.1-video-plays-day-no-data.json in Resources */, 7403A2F81EF06FEB00DED7DC /* me-settings-change-display-name-success.json in Resources */, FFE247B420C891E6002DF3A2 /* WordPressComOAuthSuccess.json in Resources */, + 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */, 74D67F1F1F15C3240010C5ED /* people-send-invitation-success.json in Resources */, FFE247B020C891E6002DF3A2 /* WordPressComAuthenticateWithIDToken2FANeededSuccess.json in Resources */, 436D563E2118E34D00CEAA33 /* supported-states-success.json in Resources */, @@ -2252,6 +2259,7 @@ 93BD277F1EE73944002BB00B /* WordPressComOAuthClient.swift in Sources */, 740B23B91F17EC7300067A2A /* PostServiceRemoteREST.m in Sources */, 93BD27801EE73944002BB00B /* WordPressComRestApi.swift in Sources */, + 404057D2221C56AB0060250C /* CountryStatsType.swift in Sources */, E11C2AD21FA77FB90023BDE2 /* SitePlugin.swift in Sources */, 74A44DCC1F13C533006CD8F4 /* NotificationSyncServiceRemote.swift in Sources */, 74E229501F1E741B0085F7F2 /* RemotePublicizeConnection.swift in Sources */, diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 85ca598a..4a2abb32 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -71,14 +71,22 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in guard - let dateString = response["date"] as? String, - let date = self.periodDataQueryDateFormatter.date(from: dateString), - let periodString = response["period"] as? String, - let parsedPeriod = StatsPeriodUnit(string: periodString), - let days = response["days"] as? [String: AnyObject], - let firstKey = days.keys.first, - let firstDay = days[firstKey] as? [String: AnyObject], - let timestats = TimeStatsType(date: date, period: parsedPeriod, jsonDictionary: firstDay) + let jsonResponse = response as? [String: AnyObject], + let dateString = jsonResponse["date"] as? String, + let date = self.periodDataQueryDateFormatter.date(from: dateString) + else { + completion(nil, ResponseError.decodingFailure) + return + } + + let periodString = jsonResponse["period"] as? String + let parsedPeriod = periodString.flatMap { StatsPeriodUnit(string: $0) } ?? period + // some responses omit this field! not a reason to fail a whole request parsing though. + + guard + let timestats = TimeStatsType(date: date, + period: parsedPeriod, + jsonDictionary: jsonResponse) else { completion(nil, ResponseError.decodingFailure) return @@ -170,6 +178,23 @@ public protocol TimeStatsProtocol { init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) } +extension TimeStatsProtocol { + + // Most of the responses for time data come in a unwieldy format, that requires awkwkard unwrapping + // at the call-site — unfortunately not _all of them_, which means we can't just do it at the request level. + static func unwrapDaysDictionary(jsonDictionary: [String: AnyObject]) -> [String: AnyObject]? { + guard + let days = jsonDictionary["days"] as? [String: AnyObject], + let firstKey = days.keys.first, + let firstDay = days[firstKey] as? [String: AnyObject] + else { + return nil + } + return firstDay + } + +} + // We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed. // For now we can piggy-back off the old type and add this as an extension. fileprivate extension StatsPeriodUnit { diff --git a/WordPressKit/Time-based data/AuthorsStatsType.swift b/WordPressKit/Time-based data/AuthorsStatsType.swift index 3fcdb60c..628ff95c 100644 --- a/WordPressKit/Time-based data/AuthorsStatsType.swift +++ b/WordPressKit/Time-based data/AuthorsStatsType.swift @@ -27,7 +27,8 @@ extension AuthorsStatsType: TimeStatsProtocol { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { guard - let authors = jsonDictionary["authors"] as? [[String: AnyObject]] + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let authors = unwrappedDays["authors"] as? [[String: AnyObject]] else { return nil } diff --git a/WordPressKit/Time-based data/CountryStatsType.swift b/WordPressKit/Time-based data/CountryStatsType.swift new file mode 100644 index 00000000..dee5b700 --- /dev/null +++ b/WordPressKit/Time-based data/CountryStatsType.swift @@ -0,0 +1,66 @@ +public struct CountryStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalViewsCount: Int + public let otherViewsCount: Int + + public let countries: [StatsCountry] +} + +public struct StatsCountry { + let name: String + let code: String + let viewsCount: Int +} + +extension CountryStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/country-views" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let countryInfo = jsonDictionary["country-info"] as? [String: AnyObject], + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let totalViews = unwrappedDays["total_views"] as? Int, + let otherViews = unwrappedDays["other_views"] as? Int, + let countriesViews = unwrappedDays["views"] as? [[String: AnyObject]] + else { + return nil + } + + self.periodEndDate = date + self.period = period + + self.totalViewsCount = totalViews + self.otherViewsCount = otherViews + self.countries = countriesViews.compactMap { StatsCountry(jsonDictionary: $0, countryInfo: countryInfo) } + } + +} + +extension StatsCountry { + init?(jsonDictionary: [String: AnyObject], countryInfo: [String: AnyObject]) { + guard + let viewsCount = jsonDictionary["views"] as? Int, + let countryCode = jsonDictionary["country_code"] as? String + else { + return nil + } + + let name: String + + if + let countryDict = countryInfo[countryCode] as? [String: AnyObject], + let countryName = countryDict["country_full"] as? String { + name = countryName + } else { + name = countryCode + } + + self.viewsCount = viewsCount + self.code = countryCode + self.name = name + } +} diff --git a/WordPressKit/Time-based data/SearchTermStatsType.swift b/WordPressKit/Time-based data/SearchTermStatsType.swift index b3238509..8c47ff3a 100644 --- a/WordPressKit/Time-based data/SearchTermStatsType.swift +++ b/WordPressKit/Time-based data/SearchTermStatsType.swift @@ -20,10 +20,11 @@ extension SearchTermStatsType: TimeStatsProtocol { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { guard - let totalSearchTerms = jsonDictionary["total_search_terms"] as? Int, - let hiddenSearchTerms = jsonDictionary["encrypted_search_terms"] as? Int, - let otherSearchTerms = jsonDictionary["other_search_terms"] as? Int, - let searchTermsDict = jsonDictionary["search_terms"] as? [[String: AnyObject]] + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let totalSearchTerms = unwrappedDays["total_search_terms"] as? Int, + let hiddenSearchTerms = unwrappedDays["encrypted_search_terms"] as? Int, + let otherSearchTerms = unwrappedDays["other_search_terms"] as? Int, + let searchTermsDict = unwrappedDays["search_terms"] as? [[String: AnyObject]] else { return nil } diff --git a/WordPressKit/Time-based data/VideosStatsType.swift b/WordPressKit/Time-based data/VideosStatsType.swift index 494f3d91..23c0317c 100644 --- a/WordPressKit/Time-based data/VideosStatsType.swift +++ b/WordPressKit/Time-based data/VideosStatsType.swift @@ -24,9 +24,10 @@ extension VideoStatsType: TimeStatsProtocol { public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { guard - let totalPlayCount = jsonDictionary["total_plays"] as? Int, - let otherPlays = jsonDictionary["other_plays"] as? Int, - let videos = jsonDictionary["plays"] as? [[String: AnyObject]] + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let totalPlayCount = unwrappedDays["total_plays"] as? Int, + let otherPlays = unwrappedDays["other_plays"] as? Int, + let videos = unwrappedDays["plays"] as? [[String: AnyObject]] else { return nil } diff --git a/WordPressKitTests/Mock Data/stats-countries-data.json b/WordPressKitTests/Mock Data/stats-countries-data.json new file mode 100644 index 00000000..d69290db --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-countries-data.json @@ -0,0 +1,113 @@ +{ + "date": "2018-12-31", + "days": { + "2018-01-01": { + "views": [ + { + "country_code": "US", + "views": 937 + }, + { + "country_code": "GB", + "views": 165 + }, + { + "country_code": "IN", + "views": 159 + }, + { + "country_code": "AU", + "views": 107 + }, + { + "country_code": "ES", + "views": 63 + }, + { + "country_code": "CA", + "views": 47 + }, + { + "country_code": "GR", + "views": 45 + }, + { + "country_code": "HK", + "views": 42 + }, + { + "country_code": "SG", + "views": 40 + }, + { + "country_code": "NL", + "views": 37 + } + ], + "other_views": 242, + "total_views": 1884 + } + }, + "country-info": { + "US": { + "flag_icon": "https://secure.gravatar.com/blavatar/5a83891a81b057fed56930a6aaaf7b3c?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/9f4faa5ad0c723474f7a6d810172447c?s=48", + "country_full": "United States", + "map_region": "021" + }, + "GB": { + "flag_icon": "https://secure.gravatar.com/blavatar/45d1fd3f398678452fd02153f569ce01?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/85ac446c6eefc7e959e15a6877046da3?s=48", + "country_full": "United Kingdom", + "map_region": "154" + }, + "IN": { + "flag_icon": "https://secure.gravatar.com/blavatar/217b6ac82c316e3a176351cef1d2d0b6?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/d449a857f065ec5ddf1e7a086001a541?s=48", + "country_full": "India", + "map_region": "034" + }, + "AU": { + "flag_icon": "https://secure.gravatar.com/blavatar/652b8d886abb355153e7486c43f7d3d4?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/aae10060ced491841b196f526a8e9d1b?s=48", + "country_full": "Australia", + "map_region": "053" + }, + "ES": { + "flag_icon": "https://secure.gravatar.com/blavatar/3b0fec88bde4fc30fd3e23e4ccd99f82?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/365c1d4da35ae303b9c5b2456fb83783?s=48", + "country_full": "Spain", + "map_region": "039" + }, + "CA": { + "flag_icon": "https://secure.gravatar.com/blavatar/7f3085b2665ac78346be5923724ba4c6?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/685ac009247bf3378158ee41c3f8f250?s=48", + "country_full": "Canada", + "map_region": "021" + }, + "GR": { + "flag_icon": "https://secure.gravatar.com/blavatar/b6b7e68f84a52ab815467a6fbec1f3c0?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/9b9c3f808361ec2e84526c44eb42944c?s=48", + "country_full": "Greece", + "map_region": "039" + }, + "HK": { + "flag_icon": "https://secure.gravatar.com/blavatar/1a989835b3db99915802fcba781dded1?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/50afdd95210bdca099002a4d64eedb44?s=48", + "country_full": "Hong Kong SAR China", + "map_region": "030" + }, + "SG": { + "flag_icon": "https://secure.gravatar.com/blavatar/2e92f433832ac16cdbace58dd80237fd?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/7bf576be119dfc4b569c068db84f56a5?s=48", + "country_full": "Singapore", + "map_region": "035" + }, + "NL": { + "flag_icon": "https://secure.gravatar.com/blavatar/a7c9a0e4e9006eadb21ca448f54d990e?s=48", + "flat_flag_icon": "https://secure.gravatar.com/blavatar/646cb56a79f9aee2e85999232ad7db45?s=48", + "country_full": "Netherlands", + "map_region": "155" + } + } +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index abfe4dcf..0ab2ae92 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -12,6 +12,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getSearchDataFilename = "stats-search-term-result.json" let getAuthorsDataFilename = "stats-top-authors.json" let getVideosMockFilename = "stats-videos-data.json" + let getCountriesMockFilename = "stats-countries-data.json" // MARK: - Properties @@ -19,6 +20,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { var siteSearchDataEndpoint: String { return "sites/\(siteID)/stats/search-terms/" } var siteAuthorsDataEndpoint: String { return "sites/\(siteID)/stats/top-authors/" } var siteVideosDataEndpoint: String { return "sites/\(siteID)/stats/video-plays/" } + var siteCountriesDataEndpoint: String { return "sites/\(siteID)/stats/country-views/" } var remote: StatsServiceRemoteV2! @@ -147,4 +149,38 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testCountries() { + let expect = expectation(description: "It should return country data for a year") + + stubRemoteResponse(siteCountriesDataEndpoint, filename: getCountriesMockFilename, contentType: .ApplicationJSON) + + let dec31 = DateComponents(year: 2018, month: 12, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: dec31)! + + + remote.getData(for: .year, endingOn: date) { (countries: CountryStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(countries) + + XCTAssertEqual(countries?.totalViewsCount, 1884) + XCTAssertEqual(countries?.otherViewsCount, 242) + + XCTAssertEqual(countries?.countries.count, 10) + + XCTAssertEqual(countries?.countries.first!.viewsCount, 937) + XCTAssertEqual(countries?.countries.first!.name, "United States") + XCTAssertEqual(countries?.countries.first!.code, "US") + + XCTAssertEqual(countries?.countries.last!.viewsCount, 37) + XCTAssertEqual(countries?.countries.last!.name, "Netherlands") + XCTAssertEqual(countries?.countries.last!.code, "NL") + + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + } } From 280b4d7cad751da7b2b8e5f736f337151b74915c Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 21:16:44 +0100 Subject: [PATCH 08/27] Add support for fetching Clicks data --- WordPressKit.xcodeproj/project.pbxproj | 8 + .../Time-based data/ClicksStatsType.swift | 71 ++++++ .../Mock Data/stats-clicks-data.json | 231 ++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 42 ++++ 4 files changed, 352 insertions(+) create mode 100644 WordPressKit/Time-based data/ClicksStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-clicks-data.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 91ff7115..fab4c4bf 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 404057D0221C46790060250C /* stats-videos-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057CF221C46780060250C /* stats-videos-data.json */; }; 404057D2221C56AB0060250C /* CountryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057D1221C56AB0060250C /* CountryStatsType.swift */; }; 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057D3221C5FC30060250C /* stats-countries-data.json */; }; + 404057D6221C92660060250C /* ClicksStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057D5221C92660060250C /* ClicksStatsType.swift */; }; + 404057D8221C986A0060250C /* stats-clicks-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057D7221C98690060250C /* stats-clicks-data.json */; }; 4041405E220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */; }; 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -506,6 +508,8 @@ 404057CF221C46780060250C /* stats-videos-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-videos-data.json"; sourceTree = ""; }; 404057D1221C56AB0060250C /* CountryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryStatsType.swift; sourceTree = ""; }; 404057D3221C5FC30060250C /* stats-countries-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-countries-data.json"; sourceTree = ""; }; + 404057D5221C92660060250C /* ClicksStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClicksStatsType.swift; sourceTree = ""; }; + 404057D7221C98690060250C /* stats-clicks-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-clicks-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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -1010,6 +1014,7 @@ 404057C8221B789B0060250C /* AuthorsStatsType.swift */, 404057CD221C38130060250C /* VideosStatsType.swift */, 404057D1221C56AB0060250C /* CountryStatsType.swift */, + 404057D5221C92660060250C /* ClicksStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1527,6 +1532,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 404057D7221C98690060250C /* stats-clicks-data.json */, 404057D3221C5FC30060250C /* stats-countries-data.json */, 404057CF221C46780060250C /* stats-videos-data.json */, 404057CA221B80BC0060250C /* stats-top-authors.json */, @@ -2040,6 +2046,7 @@ 74C473C71EF334D4009918F2 /* site-active-purchases-none-active-success.json in Resources */, 829BA4321FACF187003ADEEA /* activity-rewind-status-restore-finished.json in Resources */, 74D67F181F15C2D70010C5ED /* site-users-update-role-unknown-site-failure.json in Resources */, + 404057D8221C986A0060250C /* stats-clicks-data.json in Resources */, 436D56532121F60500CEAA33 /* supported-states-empty.json in Resources */, 93AC8ED31ED32FD000900F5A /* stats-v1.1-streak.json in Resources */, 7403A2F91EF06FEB00DED7DC /* me-settings-change-email-success.json in Resources */, @@ -2284,6 +2291,7 @@ 93BD277E1EE73944002BB00B /* NSDate+WordPressJSON.m in Sources */, 7E3E7A4820E443370075D159 /* NSMutableAttributedString+extensions.swift in Sources */, E194CB731FBDEF6500B0A8B8 /* PluginState.swift in Sources */, + 404057D6221C92660060250C /* ClicksStatsType.swift in Sources */, 9AF4F2FC218331DC00570E4B /* PostServiceRemoteREST+Revisions.swift in Sources */, E13EE1471F33258E00C15787 /* PluginServiceRemote.swift in Sources */, 9368C7B61EC630270092CE8E /* StatsStreak.m in Sources */, diff --git a/WordPressKit/Time-based data/ClicksStatsType.swift b/WordPressKit/Time-based data/ClicksStatsType.swift new file mode 100644 index 00000000..face4b7b --- /dev/null +++ b/WordPressKit/Time-based data/ClicksStatsType.swift @@ -0,0 +1,71 @@ +public struct ClicksStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalClicksCount: Int + public let otherClicksCount: Int + + public let clicks: [StatsClick] +} + +public struct StatsClick { + public let title: String + public let clicksCount: Int + public let clickedURL: URL? + public let iconURL: URL? + + public let children: [StatsClick] +} + +extension ClicksStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/clicks" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let totalClicks = unwrappedDays["total_clicks"] as? Int, + let otherClicks = unwrappedDays["other_clicks"] as? Int, + let clicks = unwrappedDays["clicks"] as? [[String: AnyObject]] + else { + return nil + } + + + + self.period = period + self.periodEndDate = date + self.totalClicksCount = totalClicks + self.otherClicksCount = otherClicks + self.clicks = clicks.compactMap { StatsClick(jsonDictionary: $0) } + } +} + +extension StatsClick { + init?(jsonDictionary: [String: AnyObject]) { + guard + let title = jsonDictionary["name"] as? String, + let clicksCount = jsonDictionary["views"] as? Int + else { + return nil + } + + let children: [StatsClick] + + if let childrenJSON = jsonDictionary["children"] as? [[String: AnyObject]] { + children = childrenJSON.compactMap { StatsClick(jsonDictionary: $0) } + } else { + children = [] + } + + let icon = jsonDictionary["icon"] as? String + let urlString = jsonDictionary["url"] as? String + + self.title = title + self.clicksCount = clicksCount + self.clickedURL = urlString.flatMap { URL(string: $0) } + self.iconURL = icon.flatMap { URL(string: $0) } + self.children = children + } +} diff --git a/WordPressKitTests/Mock Data/stats-clicks-data.json b/WordPressKitTests/Mock Data/stats-clicks-data.json new file mode 100644 index 00000000..fa21876b --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-clicks-data.json @@ -0,0 +1,231 @@ +{ + "date": "2018-12-31", + "days": { + "2018-01-01": { + "clicks": [ + { + "icon": "https://secure.gravatar.com/blavatar/70ac4b986ed274e446bd33c2fdeefe49?s=48", + "url": "http://automattic.com/work-with-us/?utm_source=a8c&utm_medium=site&utm_campaign=officetoday", + "name": "automattic.com/work-with-us/?utm_source=a8c&utm_medium=site&utm_campaign=officetoday", + "views": 767, + "children": null + }, + { + "icon": null, + "url": null, + "name": "WordPress.com Media ", + "views": 167, + "children": [ + { + "url": "https://officetoday.files.wordpress.com/2018/11/20181115_093040.jpg", + "name": "officetoday.files.wordpress.com/2018/11/20181115_093040.jpg", + "views": 22 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/06/20180603_070714.jpg", + "name": "officetoday.files.wordpress.com/2018/06/20180603_070714.jpg", + "views": 16 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/03/mvimg_20180318_175544.jpg", + "name": "officetoday.files.wordpress.com/2018/03/mvimg_20180318_175544.jpg", + "views": 10 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/10/p_20181018_112554.jpg", + "name": "officetoday.files.wordpress.com/2018/10/p_20181018_112554.jpg", + "views": 8 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/01/table-mountain.jpg", + "name": "officetoday.files.wordpress.com/2018/01/table-mountain.jpg", + "views": 8 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/10/img_7618.jpg", + "name": "officetoday.files.wordpress.com/2018/10/img_7618.jpg", + "views": 7 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/05/img_6877.jpg", + "name": "officetoday.files.wordpress.com/2018/05/img_6877.jpg", + "views": 6 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/10/img_0338.jpg", + "name": "officetoday.files.wordpress.com/2018/10/img_0338.jpg", + "views": 6 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/08/imag1522.jpg", + "name": "officetoday.files.wordpress.com/2018/08/imag1522.jpg", + "views": 6 + }, + { + "url": "https://officetoday.files.wordpress.com/2018/07/image001.jpeg", + "name": "officetoday.files.wordpress.com/2018/07/image001.jpeg", + "views": 6 + } + ] + }, + { + "icon": "https://secure.gravatar.com/blavatar/81012615abd544ea5d9a3cede8da79bc?s=48", + "url": null, + "name": "gravatar.com", + "views": 48, + "children": [ + { + "url": "http://gravatar.com/madeincosmos", + "name": "gravatar.com/madeincosmos", + "views": 2 + }, + { + "url": "http://gravatar.com/da029fa4d07f05dace95562631d6e66c", + "name": "gravatar.com/da029fa4d07f05dace95562631d6e66c", + "views": 2 + }, + { + "url": "http://gravatar.com/karenalma", + "name": "gravatar.com/karenalma", + "views": 2 + }, + { + "url": "http://gravatar.com/22bd03ace6f176bfe0c593650bcf45d8", + "name": "gravatar.com/22bd03ace6f176bfe0c593650bcf45d8", + "views": 2 + }, + { + "url": "http://gravatar.com/nickmomrik", + "name": "gravatar.com/nickmomrik", + "views": 1 + }, + { + "url": "http://gravatar.com/198723e26f9350d9bbe8d4f35a8b0bb7", + "name": "gravatar.com/198723e26f9350d9bbe8d4f35a8b0bb7", + "views": 1 + }, + { + "url": "http://gravatar.com/d543983924773baf644f258497b00009", + "name": "gravatar.com/d543983924773baf644f258497b00009", + "views": 1 + }, + { + "url": "http://gravatar.com/7c6a2da9082294cca87ec2f0908da9cd", + "name": "gravatar.com/7c6a2da9082294cca87ec2f0908da9cd", + "views": 1 + }, + { + "url": "http://gravatar.com/0123ce2eebe978d3c8f36a0095c77127", + "name": "gravatar.com/0123ce2eebe978d3c8f36a0095c77127", + "views": 1 + }, + { + "url": "http://gravatar.com/ea81a72572244d5cd0af5c2d3f20d5ec", + "name": "gravatar.com/ea81a72572244d5cd0af5c2d3f20d5ec", + "views": 1 + } + ] + }, + { + "icon": "https://secure.gravatar.com/blavatar/653166773dc88127bd3afe0b6dfe5ea7?s=48", + "url": null, + "name": "wordpress.com", + "views": 25, + "children": [ + { + "url": "https://wordpress.com/?ref=footer_blog", + "name": "wordpress.com/?ref=footer_blog", + "views": 14 + }, + { + "url": "https://wordpress.com/following/edit", + "name": "wordpress.com/following/edit", + "views": 4 + }, + { + "url": "https://wordpress.com/", + "name": "wordpress.com", + "views": 3 + }, + { + "url": "https://wordpress.com/read/blogs/87293438/posts/2068", + "name": "wordpress.com/read/blogs/87293438/posts/2068", + "views": 1 + }, + { + "url": "https://wordpress.com/theme/cubic/", + "name": "wordpress.com/theme/cubic/", + "views": 1 + }, + { + "url": "https://wordpress.com/start/", + "name": "wordpress.com/start/", + "views": 1 + }, + { + "url": "https://wordpress.com/read/feeds/34602574", + "name": "wordpress.com/read/feeds/34602574", + "views": 1 + } + ] + }, + { + "icon": null, + "url": "http://wifitribe.co/", + "name": "wifitribe.co", + "views": 12, + "children": null + }, + { + "icon": null, + "url": "https://www.petridish.co.nz/", + "name": "petridish.co.nz", + "views": 4, + "children": null + }, + { + "icon": null, + "url": "http://www.orangecoworking.com/", + "name": "orangecoworking.com", + "views": 2, + "children": null + }, + { + "icon": null, + "url": "https://www.dunedin.art.museum/", + "name": "dunedin.art.museum", + "views": 2, + "children": null + }, + { + "icon": "https://secure.gravatar.com/blavatar/90f2527e399855d3bc583b65f35821e7?s=48", + "url": null, + "name": "en.gravatar.com", + "views": 2, + "children": [ + { + "url": "https://en.gravatar.com/nbachiyski", + "name": "en.gravatar.com/nbachiyski", + "views": 1 + }, + { + "url": "https://en.gravatar.com/amandariu", + "name": "en.gravatar.com/amandariu", + "views": 1 + } + ] + }, + { + "icon": null, + "url": "https://www.meetup.com/Ann-Arbor-Software-Co-Workers/", + "name": "meetup.com/Ann-Arbor-Software-Co-Workers/", + "views": 1, + "children": null + } + ], + "other_clicks": 2, + "total_clicks": 1032 + } + }, + "period": "year" +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 0ab2ae92..621f4eea 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -13,6 +13,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getAuthorsDataFilename = "stats-top-authors.json" let getVideosMockFilename = "stats-videos-data.json" let getCountriesMockFilename = "stats-countries-data.json" + let getClicksMockFilename = "stats-clicks-data.json" // MARK: - Properties @@ -21,6 +22,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { var siteAuthorsDataEndpoint: String { return "sites/\(siteID)/stats/top-authors/" } var siteVideosDataEndpoint: String { return "sites/\(siteID)/stats/video-plays/" } var siteCountriesDataEndpoint: String { return "sites/\(siteID)/stats/country-views/" } + var siteClicksDataEndpoint: String { return "sites/\(siteID)/stats/clicks/" } var remote: StatsServiceRemoteV2! @@ -180,6 +182,46 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { expect.fulfill() } + waitForExpectations(timeout: timeout, handler: nil) + } + + func testClicks() { + let expect = expectation(description: "It should return clicks data for a year") + + stubRemoteResponse(siteClicksDataEndpoint, filename: getClicksMockFilename, contentType: .ApplicationJSON) + + let dec31 = DateComponents(year: 2018, month: 12, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: dec31)! + + + remote.getData(for: .year, endingOn: date) { (clicks: ClicksStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(clicks) + + XCTAssertEqual(clicks?.totalClicksCount, 1032) + XCTAssertEqual(clicks?.otherClicksCount, 2) + + XCTAssertEqual(clicks!.clicks.count, 10) + + XCTAssertEqual(clicks?.clicks.first!.clicksCount, 767) + XCTAssertEqual(clicks?.clicks.first!.title, "automattic.com/work-with-us/?utm_source=a8c&utm_medium=site&utm_campaign=officetoday") + XCTAssertEqual(clicks?.clicks.first!.clickedURL, URL(string: "http://automattic.com/work-with-us/?utm_source=a8c&utm_medium=site&utm_campaign=officetoday")) + XCTAssertEqual(clicks?.clicks.first?.iconURL, URL(string: "https://secure.gravatar.com/blavatar/70ac4b986ed274e446bd33c2fdeefe49?s=48")) + XCTAssertEqual(clicks?.clicks.first?.children.count, 0) + + XCTAssertEqual(clicks?.clicks[1].clicksCount, 167) + XCTAssertEqual(clicks?.clicks[1].title, "WordPress.com Media ") + XCTAssertNil(clicks?.clicks[1].iconURL) + XCTAssertNil(clicks?.clicks[1].clickedURL) + + XCTAssertEqual(clicks?.clicks[1].children.count, 10) + XCTAssertEqual(clicks?.clicks[1].children.first?.clicksCount, 22) + XCTAssertEqual(clicks?.clicks[1].children.first?.clickedURL, URL(string: "https://officetoday.files.wordpress.com/2018/11/20181115_093040.jpg")) + XCTAssertEqual(clicks?.clicks[1].children.first?.title, "officetoday.files.wordpress.com/2018/11/20181115_093040.jpg") + + expect.fulfill() + } + waitForExpectations(timeout: timeout, handler: nil) } From ff42caca76847d27e54fad348f07c894e0b569c2 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Tue, 19 Feb 2019 21:59:48 +0100 Subject: [PATCH 09/27] Add support for fetching clicks and referrer data --- WordPressKit.xcodeproj/project.pbxproj | 8 + .../Time-based data/ReferrerStatsType.swift | 84 +++++ .../Mock Data/stats-referrer-data.json | 313 ++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 63 ++++ 4 files changed, 468 insertions(+) create mode 100644 WordPressKit/Time-based data/ReferrerStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-referrer-data.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index fab4c4bf..48faef8a 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 404057D4221C5FC40060250C /* stats-countries-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057D3221C5FC30060250C /* stats-countries-data.json */; }; 404057D6221C92660060250C /* ClicksStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057D5221C92660060250C /* ClicksStatsType.swift */; }; 404057D8221C986A0060250C /* stats-clicks-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 404057D7221C98690060250C /* stats-clicks-data.json */; }; + 404057DA221C9D560060250C /* ReferrerStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 404057D9221C9D560060250C /* ReferrerStatsType.swift */; }; + 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 */; }; 40A71C6E220E1D8E002E3D25 /* StatsServiceRemoteV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */; }; @@ -510,6 +512,8 @@ 404057D3221C5FC30060250C /* stats-countries-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-countries-data.json"; sourceTree = ""; }; 404057D5221C92660060250C /* ClicksStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClicksStatsType.swift; sourceTree = ""; }; 404057D7221C98690060250C /* stats-clicks-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-clicks-data.json"; sourceTree = ""; }; + 404057D9221C9D560060250C /* ReferrerStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReferrerStatsType.swift; sourceTree = ""; }; + 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 = ""; }; 40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = ""; }; @@ -1015,6 +1019,7 @@ 404057CD221C38130060250C /* VideosStatsType.swift */, 404057D1221C56AB0060250C /* CountryStatsType.swift */, 404057D5221C92660060250C /* ClicksStatsType.swift */, + 404057D9221C9D560060250C /* ReferrerStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1532,6 +1537,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 404057DB221C9FD70060250C /* stats-referrer-data.json */, 404057D7221C98690060250C /* stats-clicks-data.json */, 404057D3221C5FC30060250C /* stats-countries-data.json */, 404057CF221C46780060250C /* stats-videos-data.json */, @@ -2043,6 +2049,7 @@ 74D67F201F15C3240010C5ED /* people-validate-invitation-failure.json in Resources */, 74D67F161F15C2D70010C5ED /* site-users-update-role-bad-json-failure.json in Resources */, 93BD27661EE73442002BB00B /* me-success.json in Resources */, + 404057DC221C9FD80060250C /* stats-referrer-data.json in Resources */, 74C473C71EF334D4009918F2 /* site-active-purchases-none-active-success.json in Resources */, 829BA4321FACF187003ADEEA /* activity-rewind-status-restore-finished.json in Resources */, 74D67F181F15C2D70010C5ED /* site-users-update-role-unknown-site-failure.json in Resources */, @@ -2307,6 +2314,7 @@ 7430C9B81F1927C50051B8E6 /* RemoteReaderTopic.m in Sources */, 7403A3021EF0726E00DED7DC /* AccountSettings.swift in Sources */, 40E7FEA9220FA4060032834E /* StatsEmailFollowersInsight.swift in Sources */, + 404057DA221C9D560060250C /* ReferrerStatsType.swift in Sources */, 9368C7C01EC630CE0092CE8E /* StatsStringUtilities.m in Sources */, 826016F11F9FA13A00533B6C /* ActivityServiceRemote.swift in Sources */, 74BA04FA1F06DC3900ED5CD8 /* RemoteComment.m in Sources */, diff --git a/WordPressKit/Time-based data/ReferrerStatsType.swift b/WordPressKit/Time-based data/ReferrerStatsType.swift new file mode 100644 index 00000000..4c1471a7 --- /dev/null +++ b/WordPressKit/Time-based data/ReferrerStatsType.swift @@ -0,0 +1,84 @@ +public struct ReferrerStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalReferrerViews: Int + public let otherReferrerViews: Int + + public let referrers: [StatsReferrer] +} + +public struct StatsReferrer { + public let title: String + public let viewsCount: Int + public let url: URL? + public let iconURL: URL? + + public let children: [StatsReferrer] +} + +extension ReferrerStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/referrers" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let totalClicks = unwrappedDays["total_views"] as? Int, + let otherClicks = unwrappedDays["other_views"] as? Int, + let referrers = unwrappedDays["groups"] as? [[String: AnyObject]] + else { + return nil + } + + self.period = period + self.periodEndDate = date + self.totalReferrerViews = totalClicks + self.otherReferrerViews = otherClicks + self.referrers = referrers.compactMap { StatsReferrer(jsonDictionary: $0) } + } +} + +extension StatsReferrer { + init?(jsonDictionary: [String: AnyObject]) { + guard + let title = jsonDictionary["name"] as? String + else { + return nil + } + + // The shape of API reply here is _almost_ a perfectly fractal tree structure. + // However, sometimes the keys for children/parents representing the same values change, hence this + // rether ugly hack. + let viewsCount: Int + + if let views = jsonDictionary["total"] as? Int { + viewsCount = views + } else if let views = jsonDictionary["views"] as? Int { + viewsCount = views + } else { + // If neither key is present, this is a malformed response. + return nil + } + + let children: [StatsReferrer] + + if let childrenJSON = jsonDictionary["results"] as? [[String: AnyObject]] { + children = childrenJSON.compactMap { StatsReferrer(jsonDictionary: $0) } + } else if let childrenJSON = jsonDictionary["children"] as? [[String: AnyObject]] { + children = childrenJSON.compactMap { StatsReferrer(jsonDictionary: $0) } + } else { + children = [] + } + + let icon = jsonDictionary["icon"] as? String + let urlString = jsonDictionary["url"] as? String + + self.title = title + self.viewsCount = viewsCount + self.url = urlString.flatMap { URL(string: $0) } + self.iconURL = icon.flatMap { URL(string: $0) } + self.children = children + } +} diff --git a/WordPressKitTests/Mock Data/stats-referrer-data.json b/WordPressKitTests/Mock Data/stats-referrer-data.json new file mode 100644 index 00000000..98dc1302 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-referrer-data.json @@ -0,0 +1,313 @@ +{ + "date": "2019-01-31", + "days": { + "2019-01-01": { + "groups": [ + { + "group": "linkedin.com", + "name": "linkedin.com", + "icon": "https://secure.gravatar.com/blavatar/f54db463750940e0e7f7630fe327845e?s=48", + "total": 126, + "follow_data": null, + "results": [ + { + "name": "linkedin.com", + "url": "https://www.linkedin.com/", + "views": 72 + }, + { + "name": "linkedin.com/feed/", + "url": "https://www.linkedin.com/feed/", + "views": 47 + }, + { + "name": "linkedin.com/company/automattic/", + "url": "https://www.linkedin.com/company/automattic/", + "views": 5 + }, + { + "name": "linkedin.com/in/uruinusa/detail/recent-activity/", + "url": "https://www.linkedin.com/in/uruinusa/detail/recent-activity/", + "views": 1 + }, + { + "name": "linkedin.com/feed/?trk=hb-0-h-logo", + "url": "https://www.linkedin.com/feed/?trk=hb-0-h-logo", + "views": 1 + } + ] + }, + { + "group": "twitter.com", + "name": "Twitter", + "url": "http://twitter.com/", + "icon": "https://secure.gravatar.com/blavatar/7905d1c4e12c54933a44d19fcd5f9356?s=48", + "total": 124, + "follow_data": null, + "results": { + "views": 124 + } + }, + { + "group": "WordPress.com Reader", + "name": "WordPress.com Reader", + "url": "https://wordpress.com/", + "icon": "https://secure.gravatar.com/blavatar/236c008da9dc0edb4b3464ecebb3fc1d?s=48", + "total": 103, + "follow_data": null, + "results": { + "views": 103 + } + }, + { + "group": "Search Engines", + "name": "Search Engines", + "icon": "https://wordpress.com/i/stats/search-engine.png", + "total": 55, + "follow_data": null, + "results": [ + { + "name": "Google Search", + "icon": "https://secure.gravatar.com/blavatar/6741a05f4bc6e5b65f504c4f3df388a1?s=48", + "views": 55, + "children": [ + { + "name": "google.com", + "url": "http://www.google.com/", + "icon": null, + "views": 47 + }, + { + "name": "google.hu", + "url": "http://www.google.hu", + "icon": "https://secure.gravatar.com/blavatar/734e0c8cb739a8577f6c5adca0675ac9?s=48", + "views": 1 + }, + { + "name": "google.ro", + "url": "http://www.google.ro", + "icon": "https://secure.gravatar.com/blavatar/4cf9c503ae564c0c56e96c6ac5b0850a?s=48", + "views": 1 + }, + { + "name": "google.de", + "url": "http://www.google.de", + "icon": "https://secure.gravatar.com/blavatar/108eca7b644e2c5e09853619bc416ed0?s=48", + "views": 1 + }, + { + "name": "google.com.br", + "url": "http://www.google.com.br", + "icon": "https://secure.gravatar.com/blavatar/529a6e3ab932e602843d1e6f5fc384c6?s=48", + "views": 1 + }, + { + "name": "google.es", + "url": "http://www.google.es", + "icon": "https://secure.gravatar.com/blavatar/7ec3dec14a88a3b50bab9a2b2d8e9e83?s=48", + "views": 1 + }, + { + "name": "google.ca", + "url": "http://www.google.ca", + "icon": "https://secure.gravatar.com/blavatar/3eac48a51cb5302e35fe68a819220647?s=48", + "views": 1 + }, + { + "name": "google.co.uk", + "url": "http://www.google.co.uk", + "icon": "https://secure.gravatar.com/blavatar/d5d4cf8ec8dc8fddc90b7024afa3ddb3?s=48", + "views": 1 + }, + { + "name": "google.co.za", + "url": "http://www.google.co.za", + "icon": "https://secure.gravatar.com/blavatar/260289fb0e63d27a83fb63a1f5449806?s=48", + "views": 1 + } + ] + } + ] + }, + { + "group": "facebook.com", + "name": "Facebook", + "url": "http://facebook.com/", + "icon": "https://secure.gravatar.com/blavatar/2343ec78a04c6ea9d80806345d31fd78?s=48", + "total": 49, + "follow_data": null, + "results": { + "views": 49 + } + }, + { + "group": "android-app", + "name": "WordPress Android App", + "icon": null, + "total": 43, + "follow_data": null, + "results": [ + { + "name": "com.linkedin.android", + "url": "http://android-app://com.linkedin.android", + "views": 24 + }, + { + "name": "com.google.android.gm", + "url": "http://android-app://com.google.android.gm", + "views": 15 + }, + { + "name": "com.slack", + "url": "http://android-app://com.slack", + "views": 2 + }, + { + "name": "com.google.android.googlequicksearchbox", + "url": "http://android-app://com.google.android.googlequicksearchbox", + "views": 1 + }, + { + "name": "m.facebook.com", + "url": "http://android-app://m.facebook.com", + "views": 1 + } + ] + }, + { + "group": "happinessengineer.blog", + "name": "happinessengineer.blog", + "icon": null, + "total": 19, + "follow_data": null, + "results": [ + { + "name": "happinessengineer.blog/2018/12/18/engineering-happiness-as-a-parent/", + "url": "https://happinessengineer.blog/2018/12/18/engineering-happiness-as-a-parent/", + "views": 5 + }, + { + "name": "happinessengineer.blog", + "url": "https://happinessengineer.blog/", + "views": 4 + }, + { + "name": "happinessengineer.blog/2018/05/17/happiness-around-the-world/", + "url": "https://happinessengineer.blog/2018/05/17/happiness-around-the-world/", + "views": 3 + }, + { + "name": "happinessengineer.blog/our-jobs/", + "url": "https://happinessengineer.blog/our-jobs/", + "views": 2 + }, + { + "name": "happinessengineer.blog/2018/11/27/meet-an-enterprise-happiness-engineer-shannon-smith/", + "url": "https://happinessengineer.blog/2018/11/27/meet-an-enterprise-happiness-engineer-shannon-smith/", + "views": 2 + }, + { + "name": "happinessengineer.blog/2018/05/14/so-you-want-to-be-a-happiness-engineer-huh/", + "url": "https://happinessengineer.blog/2018/05/14/so-you-want-to-be-a-happiness-engineer-huh/", + "views": 1 + }, + { + "name": "happinessengineer.blog/2018/12/04/unusual-schedules-as-a-happiness-engineer/", + "url": "https://happinessengineer.blog/2018/12/04/unusual-schedules-as-a-happiness-engineer/", + "views": 1 + }, + { + "name": "happinessengineer.blog/2018/05/15/diversematticians/", + "url": "https://happinessengineer.blog/2018/05/15/diversematticians/", + "views": 1 + } + ] + }, + { + "group": "piszek.com", + "name": "piszek.com", + "icon": null, + "total": 9, + "follow_data": null, + "results": [ + { + "name": "piszek.com/2016/04/05/automattic/", + "url": "https://piszek.com/2016/04/05/automattic/", + "views": 7 + }, + { + "name": "piszek.com/2016/04/05/automattic/amp/", + "url": "https://piszek.com/2016/04/05/automattic/amp/", + "views": 2 + } + ] + }, + { + "group": "vip.wordpress.com", + "name": "vip.wordpress.com", + "icon": "https://secure.gravatar.com/blavatar/c0a70310ea07fb03e415f74916b37b35?s=48", + "total": 8, + "follow_data": { + "params": { + "stat-source": "stats_referrer", + "follow-text": "Follow", + "following-text": "Following", + "following-hover-text": "Unfollow", + "blog_domain": "vip.wordpress.com", + "blog_url": "http://vip.wordpress.com", + "blog_id": 2235322, + "site_id": 2235322, + "blog_title": "Enterprise WordPress hosting, support, and consulting - WordPress VIP", + "is_following": false + }, + "type": "follow" + }, + "results": [ + { + "name": "vip.wordpress.com/2018/11/", + "url": "https://vip.wordpress.com/2018/11/", + "views": 3 + }, + { + "name": "vip.wordpress.com/2017/10/06/the-dream-internship-work-at-automattic-winter-2018-and-beyond/", + "url": "https://vip.wordpress.com/2017/10/06/the-dream-internship-work-at-automattic-winter-2018-and-beyond/", + "views": 3 + }, + { + "name": "vip.wordpress.com/mentorship/", + "url": "https://vip.wordpress.com/mentorship/", + "views": 1 + }, + { + "name": "vip.wordpress.com/news/", + "url": "https://vip.wordpress.com/news/", + "views": 1 + } + ] + }, + { + "group": "mail.google.com", + "name": "mail.google.com", + "icon": null, + "total": 6, + "follow_data": null, + "results": [ + { + "name": "mail.google.com/mail/u/0/", + "url": "https://mail.google.com/mail/u/0/", + "views": 5 + }, + { + "name": "mail.google.com/mail/mu/mp/43/", + "url": "https://mail.google.com/mail/mu/mp/43/", + "views": 1 + } + ] + } + ], + "other_views": 18, + "total_views": 560 + } + }, + "period": "month" +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 621f4eea..ad55f1ee 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -14,6 +14,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getVideosMockFilename = "stats-videos-data.json" let getCountriesMockFilename = "stats-countries-data.json" let getClicksMockFilename = "stats-clicks-data.json" + let getReferrersMockFilename = "stats-referrer-data.json" // MARK: - Properties @@ -23,6 +24,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { var siteVideosDataEndpoint: String { return "sites/\(siteID)/stats/video-plays/" } 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 remote: StatsServiceRemoteV2! @@ -223,6 +225,67 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { } waitForExpectations(timeout: timeout, handler: nil) + } + + func testReferrers() { + let expect = expectation(description: "It should return referrer data for a year") + + stubRemoteResponse(siteReferrerDataEndpoint, filename: getReferrersMockFilename, contentType: .ApplicationJSON) + + let jan31 = DateComponents(year: 2019, month: 1, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: jan31)! + + remote.getData(for: .month, endingOn: date) { (referrers: ReferrerStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(referrers) + + XCTAssertEqual(referrers?.totalReferrerViews, 560) + XCTAssertEqual(referrers?.otherReferrerViews, 18) + + XCTAssertEqual(referrers?.referrers.count, 10) + + XCTAssertEqual(referrers?.referrers.first!.viewsCount, 126) + XCTAssertEqual(referrers?.referrers.first!.title, "linkedin.com") + XCTAssertEqual(referrers?.referrers.first!.iconURL, URL(string: "https://secure.gravatar.com/blavatar/f54db463750940e0e7f7630fe327845e?s=48")) + XCTAssertEqual(referrers?.referrers.first?.children.count, 5) + XCTAssertNil(referrers?.referrers.first!.url) + + let noChildrenItem = referrers?.referrers[1] + XCTAssertNotNil(noChildrenItem) + XCTAssertEqual(noChildrenItem!.viewsCount, 124) + XCTAssertEqual(noChildrenItem!.title, "Twitter") + XCTAssertEqual(noChildrenItem!.url, URL(string: "http://twitter.com/")) + XCTAssertEqual(noChildrenItem?.iconURL, URL(string: "https://secure.gravatar.com/blavatar/7905d1c4e12c54933a44d19fcd5f9356?s=48")) + XCTAssertEqual(noChildrenItem?.children.count, 0) + + XCTAssertEqual(referrers?.referrers[3].viewsCount, 55) + XCTAssertEqual(referrers?.referrers[3].title, "Search Engines") + XCTAssertEqual(referrers?.referrers[3].iconURL, URL(string: "https://wordpress.com/i/stats/search-engine.png")) + XCTAssertEqual(referrers?.referrers[3].children.count, 1) + XCTAssertNil(referrers?.referrers[3].url) + + let google = referrers?.referrers[3].children.first + XCTAssertNotNil(google) + + XCTAssertEqual(google!.viewsCount, 55) + XCTAssertEqual(google!.title, "Google Search") + XCTAssertEqual(google!.iconURL, URL(string: "https://secure.gravatar.com/blavatar/6741a05f4bc6e5b65f504c4f3df388a1?s=48")) + XCTAssertEqual(google?.children.count, 9) + XCTAssertNil(google?.url) + + let firstGoogleChildren = google?.children.first + XCTAssertNotNil(firstGoogleChildren) + + XCTAssertEqual(firstGoogleChildren?.viewsCount, 47) + XCTAssertEqual(firstGoogleChildren?.title, "google.com") + XCTAssertEqual(firstGoogleChildren?.url, URL(string: "http://www.google.com/")) + XCTAssertEqual(firstGoogleChildren?.children.count, 0) + XCTAssertNil(firstGoogleChildren?.iconURL) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) } } From d85fab8b19980beb5806a74f1d5ed1e94d2e6cf4 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Wed, 20 Feb 2019 10:29:33 +0100 Subject: [PATCH 10/27] Address code review feedback --- WordPressKit/Time-based data/ReferrerStatsType.swift | 8 ++++---- WordPressKitTests/StatsRemoteV2Tests.swift | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/WordPressKit/Time-based data/ReferrerStatsType.swift b/WordPressKit/Time-based data/ReferrerStatsType.swift index 4c1471a7..cf0a7d31 100644 --- a/WordPressKit/Time-based data/ReferrerStatsType.swift +++ b/WordPressKit/Time-based data/ReferrerStatsType.swift @@ -2,8 +2,8 @@ public struct ReferrerStatsType { public let period: StatsPeriodUnit public let periodEndDate: Date - public let totalReferrerViews: Int - public let otherReferrerViews: Int + public let totalReferrerViewsCount: Int + public let otherReferrerViewsCount: Int public let referrers: [StatsReferrer] } @@ -34,8 +34,8 @@ extension ReferrerStatsType: TimeStatsProtocol { self.period = period self.periodEndDate = date - self.totalReferrerViews = totalClicks - self.otherReferrerViews = otherClicks + self.totalReferrerViewsCount = totalClicks + self.otherReferrerViewsCount = otherClicks self.referrers = referrers.compactMap { StatsReferrer(jsonDictionary: $0) } } } diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index ad55f1ee..9a9f6ce6 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -239,8 +239,8 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { XCTAssertNil(error) XCTAssertNotNil(referrers) - XCTAssertEqual(referrers?.totalReferrerViews, 560) - XCTAssertEqual(referrers?.otherReferrerViews, 18) + XCTAssertEqual(referrers?.totalReferrerViewsCount, 560) + XCTAssertEqual(referrers?.otherReferrerViewsCount, 18) XCTAssertEqual(referrers?.referrers.count, 10) From 522c23cb22ac87e7acf3139552c9db0e73c235f0 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Wed, 20 Feb 2019 10:30:13 +0100 Subject: [PATCH 11/27] Update podfile --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index 2a137f48..14257df3 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -70,7 +70,7 @@ SPEC CHECKSUMS: OCMock: 43565190abc78977ad44a61c0d20d7f0784d35ab OHHTTPStubs: 1e21c7d2c084b8153fc53d48400d8919d2d432d0 UIDeviceIdentifier: 8f8a24b257a4d978c8d40ad1e7355b944ffbfa8c - WordPressKit: 36e5966f63f88efc51c6a3de4aa07dbd66eb15cc + WordPressKit: 4e5f1df694d6c3065fc530df5580e0cdbeed8c7a WordPressShared: cfbda56868419842dd7a106a4e807069a0c17aa9 wpxmlrpc: 6ba55c773cfa27083ae4a2173e69b19f46da98e2 From 9e4d53540d8628fa36f092c5fb06c020bf261eaa Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Wed, 20 Feb 2019 23:02:11 +0100 Subject: [PATCH 12/27] Add support for fetching top posts --- WordPressKit.xcodeproj/project.pbxproj | 8 ++ .../Time-based data/AuthorsStatsType.swift | 25 +++++ .../Time-based data/PostsStatsType.swift | 55 ++++++++++ .../Mock Data/stats-posts-data.json | 102 ++++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 45 ++++++++ 5 files changed, 235 insertions(+) create mode 100644 WordPressKit/Time-based data/PostsStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-posts-data.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 48faef8a..654adde5 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 */; }; + 4081976F221DDE9B00A298E4 /* PostsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */; }; + 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819770221DFDB600A298E4 /* stats-posts-data.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 */; }; @@ -516,6 +518,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 = ""; }; + 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsStatsType.swift; sourceTree = ""; }; + 40819770221DFDB600A298E4 /* stats-posts-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-posts-data.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 = ""; }; @@ -1016,6 +1020,7 @@ children = ( 404057C4221B30400060250C /* SearchTermStatsType.swift */, 404057C8221B789B0060250C /* AuthorsStatsType.swift */, + 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */, 404057CD221C38130060250C /* VideosStatsType.swift */, 404057D1221C56AB0060250C /* CountryStatsType.swift */, 404057D5221C92660060250C /* ClicksStatsType.swift */, @@ -1537,6 +1542,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 40819770221DFDB600A298E4 /* stats-posts-data.json */, 404057DB221C9FD70060250C /* stats-referrer-data.json */, 404057D7221C98690060250C /* stats-clicks-data.json */, 404057D3221C5FC30060250C /* stats-countries-data.json */, @@ -2037,6 +2043,7 @@ 7403A2FD1EF06FEB00DED7DC /* me-settings-change-primary-site-success.json in Resources */, 93AC8ED41ED32FD000900F5A /* stats-v1.1-summary.json in Resources */, 74C473B71EF3229B009918F2 /* site-delete-unexpected-json-failure.json in Resources */, + 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */, 93AC8ECD1ED32FD000900F5A /* stats-v1.1-insights.json in Resources */, 740B23EE1F17FB7E00067A2A /* xmlrpc-malformed-request-xml-error.xml in Resources */, 826016F91F9FAF6300533B6C /* activity-log-success-3.json in Resources */, @@ -2293,6 +2300,7 @@ 74E2295C1F1E77290085F7F2 /* KeyringConnectionExternalUser.swift in Sources */, E1BD95151FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift in Sources */, 7430C9D71F1933210051B8E6 /* RemoteReaderCrossPostMeta.swift in Sources */, + 4081976F221DDE9B00A298E4 /* PostsStatsType.swift in Sources */, 9311A68B1F22625A00704AC9 /* TaxonomyServiceRemoteXMLRPC.m in Sources */, 40414060220F9F1F00CF7C5B /* StatsAllTimesInsight.swift in Sources */, 93BD277E1EE73944002BB00B /* NSDate+WordPressJSON.m in Sources */, diff --git a/WordPressKit/Time-based data/AuthorsStatsType.swift b/WordPressKit/Time-based data/AuthorsStatsType.swift index 628ff95c..e0ac1d06 100644 --- a/WordPressKit/Time-based data/AuthorsStatsType.swift +++ b/WordPressKit/Time-based data/AuthorsStatsType.swift @@ -14,10 +14,21 @@ public struct StatsTopAuthor { public struct StatsTopPost { + + public enum Kind { + case unknown + case post + case page + case homepage + } + + public let title: String public let postID: Int public let postURL: URL? public let viewsCount: Int + public let kind: Kind + } extension AuthorsStatsType: TimeStatsProtocol { @@ -83,6 +94,20 @@ extension StatsTopPost { self.postID = postID self.viewsCount = viewsCount self.postURL = URL(string: postURL) + self.kind = type(of: self).kind(from: jsonDictionary["type"] as? String) + } + + static func kind(from kindString: String?) -> Kind { + switch kindString { + case "post"?: + return .post + case "homepage"?: + return .homepage + case "page"?: + return .page + default: + return .unknown + } } } diff --git a/WordPressKit/Time-based data/PostsStatsType.swift b/WordPressKit/Time-based data/PostsStatsType.swift new file mode 100644 index 00000000..7f81ee64 --- /dev/null +++ b/WordPressKit/Time-based data/PostsStatsType.swift @@ -0,0 +1,55 @@ +public struct PostsStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let totalViewsCount: Int + public let otherViewsCount: Int + public let topPosts: [StatsTopPost] +} + +extension PostsStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/top-posts" + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let unwrappedDays = type(of: self).unwrapDaysDictionary(jsonDictionary: jsonDictionary), + let posts = unwrappedDays["postviews"] as? [[String: AnyObject]], + let totalViews = unwrappedDays["total_views"] as? Int, + let otherViews = unwrappedDays["other_views"] as? Int + else { + return nil + } + + self.periodEndDate = date + self.period = period + self.totalViewsCount = totalViews + self.otherViewsCount = otherViews + self.topPosts = posts.compactMap { StatsTopPost(topPostsJSONDictionary: $0) } + } +} + +private extension StatsTopPost { + + // the objects returned from this endpoint are _almost_ the same as the ones from `top-posts`, + // but with keys just subtly different enough that we need a custom init here. + init?(topPostsJSONDictionary jsonDictionary: [String: AnyObject]) { + guard + let url = jsonDictionary["href"] as? String, + let postID = jsonDictionary["id"] as? Int, + let title = jsonDictionary["title"] as? String, + let viewsCount = jsonDictionary["views"] as? Int, + let typeString = jsonDictionary["type"] as? String + else { + return nil + } + + self.title = title + self.postID = postID + self.postURL = URL(string: url) + self.viewsCount = viewsCount + self.kind = type(of: self).kind(from: typeString) + } + +} diff --git a/WordPressKitTests/Mock Data/stats-posts-data.json b/WordPressKitTests/Mock Data/stats-posts-data.json new file mode 100644 index 00000000..c72ff859 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-posts-data.json @@ -0,0 +1,102 @@ +{ + "date": "2019-01-31", + "days": { + "2019-01-01": { + "postviews": [ + { + "id": 0, + "href": "http://officetoday.wordpress.com/", + "date": null, + "title": "Home page / Archives", + "type": "homepage", + "views": 2816, + "video_play": false + }, + { + "id": 2396, + "href": "http://officetoday.wordpress.com/2019/01/04/valizas-uruguay/", + "date": "2019-01-04 11:40:27", + "title": "Valizas, Uruguay", + "type": "post", + "views": 146, + "video_play": false + }, + { + "id": 2413, + "href": "http://officetoday.wordpress.com/2019/01/24/dundee-scotland-2/", + "date": "2019-01-24 14:34:07", + "title": "Dundee, Scotland", + "type": "page", + "views": 141, + "video_play": false + }, + { + "id": 2376, + "href": "http://officetoday.wordpress.com/2018/12/24/porto-portugal/", + "date": "2018-12-24 10:43:12", + "title": "Porto, Portugal", + "type": "post", + "views": 36, + "video_play": false + }, + { + "id": 2403, + "href": "http://officetoday.wordpress.com/2019/01/09/fairport-ny/", + "date": "2019-01-09 21:52:59", + "title": "Fairport, NY", + "type": "post", + "views": 29, + "video_play": false + }, + { + "id": 2367, + "href": "http://officetoday.wordpress.com/2018/12/18/canberra-australia/", + "date": "2018-12-18 03:35:52", + "title": "Canberra, Australia", + "type": "post", + "views": 24, + "video_play": false + }, + { + "id": 2394, + "href": "http://officetoday.wordpress.com/2018/12/28/thalys-train-amsterdam-to-brussels/", + "date": "2018-12-28 10:25:55", + "title": "Thalys Train - Amsterdam to Brussels", + "type": "post", + "views": 21, + "video_play": false + }, + { + "id": 2412, + "href": "http://officetoday.wordpress.com/2019/01/20/prague-czech-republic-16/", + "date": "2019-01-20 11:17:23", + "title": "Prague, Czech Republic", + "type": "post", + "views": 19, + "video_play": false + }, + { + "id": 2384, + "href": "http://officetoday.wordpress.com/2018/12/27/odori-park-sapporo-japan/", + "date": "2018-12-27 23:05:24", + "title": "Odori Park, Sapporo, Japan", + "type": "post", + "views": 17, + "video_play": false + }, + { + "id": 2317, + "href": "http://officetoday.wordpress.com/2018/11/15/atacama-desert-chile/", + "date": "2018-11-15 13:58:16", + "title": "Atacama Desert, Chile", + "type": "post", + "views": 13, + "video_play": false + } + ], + "total_views": 3499, + "other_views": 237 + } + }, + "period": "month" +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 9a9f6ce6..230f883d 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 getPostsMockFilename = "stats-posts-data.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 sitePostsDataEndpoint: String { return "sites/\(siteID)/stats/top-posts/" } var remote: StatsServiceRemoteV2! @@ -288,4 +290,47 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testTopPosts() { + let expect = expectation(description: "It should return posts data for a year") + + stubRemoteResponse(sitePostsDataEndpoint, filename: getPostsMockFilename, contentType: .ApplicationJSON) + + let jan31 = DateComponents(year: 2019, month: 1, day: 31) + let date = Calendar.autoupdatingCurrent.date(from: jan31)! + + + remote.getData(for: .month, endingOn: date) { (topPosts: PostsStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(topPosts) + + XCTAssertEqual(topPosts?.totalViewsCount, 3499) + XCTAssertEqual(topPosts?.otherViewsCount, 237) + + XCTAssertEqual(topPosts?.topPosts.count, 10) + + XCTAssertEqual(topPosts?.topPosts.first?.viewsCount, 2816) + XCTAssertEqual(topPosts?.topPosts.first?.kind, .homepage) + XCTAssertEqual(topPosts?.topPosts.first?.title, "Home page / Archives") + XCTAssertEqual(topPosts?.topPosts.first?.postID, 0) + XCTAssertEqual(topPosts?.topPosts.first?.postURL, URL(string: "http://officetoday.wordpress.com/")) + + XCTAssertEqual(topPosts?.topPosts[1].viewsCount, 146) + XCTAssertEqual(topPosts?.topPosts[1].kind, .post) + XCTAssertEqual(topPosts?.topPosts[1].title, "Valizas, Uruguay") + XCTAssertEqual(topPosts?.topPosts[1].postID, 2396) + XCTAssertEqual(topPosts?.topPosts[1].postURL, URL(string: "http://officetoday.wordpress.com/2019/01/04/valizas-uruguay/")) + + XCTAssertEqual(topPosts?.topPosts[2].viewsCount, 141) + XCTAssertEqual(topPosts?.topPosts[2].kind, .page) + XCTAssertEqual(topPosts?.topPosts[2].title, "Dundee, Scotland") + XCTAssertEqual(topPosts?.topPosts[2].postID, 2413) + XCTAssertEqual(topPosts?.topPosts[2].postURL, URL(string: "http://officetoday.wordpress.com/2019/01/24/dundee-scotland-2/")) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + } } From 2b3570258d7a169c5a4f03bbcba892615d9cb127 Mon Sep 17 00:00:00 2001 From: Jeremy Massel Date: Wed, 20 Feb 2019 16:36:01 -0700 Subject: [PATCH 13/27] Fix a bug causing strange build issues --- Podfile | 7 ++- Podfile.lock | 20 +++----- WordPressKit.xcodeproj/project.pbxproj | 6 ++- WordPressKit/StatsLastPostInsight.swift | 68 +++++++++++++++++++++++++ WordPressKit/StatsServiceRemoteV2.swift | 68 ------------------------- 5 files changed, 84 insertions(+), 85 deletions(-) create mode 100644 WordPressKit/StatsLastPostInsight.swift diff --git a/Podfile b/Podfile index 666e0a74..70948832 100644 --- a/Podfile +++ b/Podfile @@ -10,7 +10,12 @@ plugin 'cocoapods-repo-update' ## ============= ## target 'WordPressKit' do - pod "WordPressKit", :path => "./" + pod 'Alamofire', '~> 4.7.3' + pod 'CocoaLumberjack', '3.4.2' + pod 'WordPressShared', '~> 1.4' + pod 'NSObject-SafeExpectations', '~> 0.0.3' + pod 'wpxmlrpc', '0.8.4' + pod 'UIDeviceIdentifier', '~> 1.1.4' target 'WordPressKitTests' do inherit! :search_paths diff --git a/Podfile.lock b/Podfile.lock index 14257df3..e4cf0daf 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,24 +27,21 @@ PODS: - OHHTTPStubs/Swift (6.1.0): - OHHTTPStubs/Default - UIDeviceIdentifier (1.1.4) - - WordPressKit (2.1.0-beta.2): - - Alamofire (~> 4.7.3) - - CocoaLumberjack (= 3.4.2) - - NSObject-SafeExpectations (= 0.0.3) - - UIDeviceIdentifier (~> 1.1.4) - - WordPressShared (~> 1.4) - - wpxmlrpc (= 0.8.4) - WordPressShared (1.7.0): - CocoaLumberjack (~> 3.4) - FormatterKit/TimeIntervalFormatter (= 1.8.2) - wpxmlrpc (0.8.4) DEPENDENCIES: + - Alamofire (~> 4.7.3) + - CocoaLumberjack (= 3.4.2) + - NSObject-SafeExpectations (~> 0.0.3) - OCMock (~> 3.4.2) - OHHTTPStubs (= 6.1.0) - OHHTTPStubs/Swift (= 6.1.0) - - WordPressKit (from `./`) + - UIDeviceIdentifier (~> 1.1.4) - WordPressShared (~> 1.4) + - wpxmlrpc (= 0.8.4) SPEC REPOS: https://github.com/cocoapods/specs.git: @@ -58,10 +55,6 @@ SPEC REPOS: - WordPressShared - wpxmlrpc -EXTERNAL SOURCES: - WordPressKit: - :path: "./" - SPEC CHECKSUMS: Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 CocoaLumberjack: db7cc9e464771f12054c22ff6947c5a58d43a0fd @@ -70,10 +63,9 @@ SPEC CHECKSUMS: OCMock: 43565190abc78977ad44a61c0d20d7f0784d35ab OHHTTPStubs: 1e21c7d2c084b8153fc53d48400d8919d2d432d0 UIDeviceIdentifier: 8f8a24b257a4d978c8d40ad1e7355b944ffbfa8c - WordPressKit: 4e5f1df694d6c3065fc530df5580e0cdbeed8c7a WordPressShared: cfbda56868419842dd7a106a4e807069a0c17aa9 wpxmlrpc: 6ba55c773cfa27083ae4a2173e69b19f46da98e2 -PODFILE CHECKSUM: 96aee46d567751451860e27dd476d15d79b40f44 +PODFILE CHECKSUM: e729a9f0e205763ed6d44ee59af67d0b65ddb885 COCOAPODS: 1.5.3 diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 48faef8a..680021d3 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -465,6 +465,7 @@ E6C1E8491EF21FC100D139D9 /* is-passwordless-account-no-account-found.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8471EF21FC100D139D9 /* is-passwordless-account-no-account-found.json */; }; E6C1E84A1EF21FC100D139D9 /* is-passwordless-account-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8481EF21FC100D139D9 /* is-passwordless-account-success.json */; }; E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */; }; + F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */; }; FF20AD2220B8471A00082398 /* WordPressKit.podspec in Resources */ = {isa = PBXBuildFile; fileRef = FF20AD2120B8471A00082398 /* WordPressKit.podspec */; }; FFE247A720C891D1002DF3A2 /* WordPressComOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */; }; FFE247AF20C891E6002DF3A2 /* WordPressComOAuthWrongPasswordFail.json in Resources */ = {isa = PBXBuildFile; fileRef = FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */; }; @@ -963,6 +964,7 @@ E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountServiceRemoteREST+SocialService.swift"; sourceTree = ""; }; ED05C8FF3E61D93CE5BA527E /* Pods_WordPressKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EFF80A6E6EE37118CB1DA158 /* Pods_WordPressKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsLastPostInsight.swift; sourceTree = ""; }; FF20AD2120B8471A00082398 /* WordPressKit.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = WordPressKit.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressComOAuthTests.swift; sourceTree = ""; }; FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = WordPressComOAuthWrongPasswordFail.json; sourceTree = ""; }; @@ -1045,6 +1047,7 @@ children = ( 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, + F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, ); name = V2; sourceTree = ""; @@ -2224,7 +2227,6 @@ "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${BUILT_PRODUCTS_DIR}/NSObject-SafeExpectations/NSObject_SafeExpectations.framework", "${BUILT_PRODUCTS_DIR}/UIDeviceIdentifier/UIDeviceIdentifier.framework", - "${BUILT_PRODUCTS_DIR}/WordPressKit/WordPressKit.framework", "${BUILT_PRODUCTS_DIR}/wpxmlrpc/wpxmlrpc.framework", ); name = "[CP] Embed Pods Frameworks"; @@ -2237,7 +2239,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NSObject_SafeExpectations.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UIDeviceIdentifier.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WordPressKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wpxmlrpc.framework", ); runOnlyForDeploymentPostprocessing = 0; @@ -2312,6 +2313,7 @@ 93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */, 9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */, 7430C9B81F1927C50051B8E6 /* RemoteReaderTopic.m in Sources */, + F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */, 7403A3021EF0726E00DED7DC /* AccountSettings.swift in Sources */, 40E7FEA9220FA4060032834E /* StatsEmailFollowersInsight.swift in Sources */, 404057DA221C9D560060250C /* ReferrerStatsType.swift in Sources */, diff --git a/WordPressKit/StatsLastPostInsight.swift b/WordPressKit/StatsLastPostInsight.swift new file mode 100644 index 00000000..4934b738 --- /dev/null +++ b/WordPressKit/StatsLastPostInsight.swift @@ -0,0 +1,68 @@ +import Foundation + +// Swift compiler doesn't like if this is not declared _in this file_, and refuses to compile the project. +// I'm guessing this has somethign to do with generic specialisation, but I'm not enough +// of a `swiftc` guru to really know. Leaving this in here to appease Swift gods. +// TODO: see if this is still a problem in Swift 5 mode! +public struct StatsLastPostInsight { + public let title: String + public let url: URL + public let publishedDate: Date + public let likesCount: Int + public let commentsCount: Int + public let viewsCount: Int + public let postID: Int +} + +extension StatsLastPostInsight: InsightProtocol { + + //MARK: - InsightProtocol Conformance + public static var queryProperties: [String: String] { + return ["order_by": "date", + "number": "1", + "type": "post", + "fields": "ID, title, URL, discussion, like_count, date"] + } + + public static var pathComponent: String { + return "posts/" + } + + public init?(jsonDictionary: [String: AnyObject]) { + fatalError("This shouldn't be ever called, instead init?(jsonDictionary:_ views:_) be called instead.") + } + + //MARK: - + + private static let dateFormatter = ISO8601DateFormatter() + + public init?(jsonDictionary: [String: AnyObject], views: Int) { + + guard + let title = jsonDictionary["title"] as? String, + let dateString = jsonDictionary["date"] as? String, + let urlString = jsonDictionary["URL"] as? String, + let likesCount = jsonDictionary["like_count"] as? Int, + let postID = jsonDictionary["ID"] as? Int, + let discussionDict = jsonDictionary["discussion"] as? [String: Any], + let commentsCount = discussionDict["comment_count"] as? Int + else { + return nil + } + + guard + let url = URL(string: urlString), + let date = StatsLastPostInsight.dateFormatter.date(from: dateString) + else { + return nil + } + + self.title = title.trimmingCharacters(in: CharacterSet.whitespaces).stringByDecodingXMLCharacters() + self.url = url + self.publishedDate = date + self.likesCount = likesCount + self.commentsCount = commentsCount + self.viewsCount = views + self.postID = postID + } +} diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 4a2abb32..c42a2a45 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -239,71 +239,3 @@ extension InsightProtocol { return "stats/" } } - - -// Swift compiler doesn't like if this is not declared _in this file_, and refuses to compile the project. -// I'm guessing this has somethign to do with generic specialisation, but I'm not enough -// of a `swiftc` guru to really know. Leaving this in here to appease Swift gods. -// TODO: see if this is still a problem in Swift 5 mode! -public struct StatsLastPostInsight { - public let title: String - public let url: URL - public let publishedDate: Date - public let likesCount: Int - public let commentsCount: Int - public let viewsCount: Int - public let postID: Int -} - -extension StatsLastPostInsight: InsightProtocol { - - //MARK: - InsightProtocol Conformance - public static var queryProperties: [String: String] { - return ["order_by": "date", - "number": "1", - "type": "post", - "fields": "ID, title, URL, discussion, like_count, date"] - } - - public static var pathComponent: String { - return "posts/" - } - - public init?(jsonDictionary: [String: AnyObject]) { - fatalError("This shouldn't be ever called, instead init?(jsonDictionary:_ views:_) be called instead.") - } - - //MARK: - - - private static let dateFormatter = ISO8601DateFormatter() - - public init?(jsonDictionary: [String: AnyObject], views: Int) { - - guard - let title = jsonDictionary["title"] as? String, - let dateString = jsonDictionary["date"] as? String, - let urlString = jsonDictionary["URL"] as? String, - let likesCount = jsonDictionary["like_count"] as? Int, - let postID = jsonDictionary["ID"] as? Int, - let discussionDict = jsonDictionary["discussion"] as? [String: Any], - let commentsCount = discussionDict["comment_count"] as? Int - else { - return nil - } - - guard - let url = URL(string: urlString), - let date = StatsLastPostInsight.dateFormatter.date(from: dateString) - else { - return nil - } - - self.title = title.trimmingCharacters(in: CharacterSet.whitespaces).stringByDecodingXMLCharacters() - self.url = url - self.publishedDate = date - self.likesCount = likesCount - self.commentsCount = commentsCount - self.viewsCount = views - self.postID = postID - } -} From e60a15354f97fb4b2cec1f6166b0031addb5d420 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 00:46:47 +0100 Subject: [PATCH 14/27] update podfile.lock --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index e4cf0daf..29f42644 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -66,6 +66,6 @@ SPEC CHECKSUMS: WordPressShared: cfbda56868419842dd7a106a4e807069a0c17aa9 wpxmlrpc: 6ba55c773cfa27083ae4a2173e69b19f46da98e2 -PODFILE CHECKSUM: e729a9f0e205763ed6d44ee59af67d0b65ddb885 +PODFILE CHECKSUM: 34d4f957f37c097c360d2863370ce2e5e06511cc COCOAPODS: 1.5.3 From d254e1c8f288e46ba57465908aab47f492a344be Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 00:51:06 +0100 Subject: [PATCH 15/27] Remove outdated comment --- WordPressKit/StatsLastPostInsight.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/WordPressKit/StatsLastPostInsight.swift b/WordPressKit/StatsLastPostInsight.swift index 4934b738..6c2327c0 100644 --- a/WordPressKit/StatsLastPostInsight.swift +++ b/WordPressKit/StatsLastPostInsight.swift @@ -1,9 +1,3 @@ -import Foundation - -// Swift compiler doesn't like if this is not declared _in this file_, and refuses to compile the project. -// I'm guessing this has somethign to do with generic specialisation, but I'm not enough -// of a `swiftc` guru to really know. Leaving this in here to appease Swift gods. -// TODO: see if this is still a problem in Swift 5 mode! public struct StatsLastPostInsight { public let title: String public let url: URL From ce344c19fc1a7fbf21fff35c0ab7e6d7e23dedef Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 00:51:51 +0100 Subject: [PATCH 16/27] Split out `StatsServiceRemoteV2` generic specialisations into an extension --- WordPressKit/StatsServiceRemoteV2.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index c42a2a45..1d06aff1 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -97,6 +97,9 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { completion(nil, error) }) } +} + +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. From 9979e34e2cf0c03c5a65bde50832fadc91035635 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 02:19:46 +0100 Subject: [PATCH 17/27] 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 18/27] 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 20d48e4e36e579f26927fcc0e5693ddf838f7fba Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 20:11:07 +0100 Subject: [PATCH 19/27] Add support for fetching Visits/Summary data --- WordPressKit.xcodeproj/project.pbxproj | 18 ++- .../{ => Insights}/StatsLastPostInsight.swift | 0 WordPressKit/StatsServiceRemoteV2.swift | 17 ++- .../Time-based data/SummaryStatsType.swift | 126 ++++++++++++++++++ .../Mock Data/stats-visits-day.json | 105 +++++++++++++++ .../Mock Data/stats-visits-month.json | 83 ++++++++++++ .../Mock Data/stats-visits-week.json | 105 +++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 117 +++++++++++++++- 8 files changed, 566 insertions(+), 5 deletions(-) rename WordPressKit/{ => Insights}/StatsLastPostInsight.swift (100%) create mode 100644 WordPressKit/Time-based data/SummaryStatsType.swift create mode 100644 WordPressKitTests/Mock Data/stats-visits-day.json create mode 100644 WordPressKitTests/Mock Data/stats-visits-month.json create mode 100644 WordPressKitTests/Mock Data/stats-visits-week.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 680021d3..cb377bfc 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -32,6 +32,10 @@ 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 */; }; + 40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819777221F00E600A298E4 /* SummaryStatsType.swift */; }; + 4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819779221F153A00A298E4 /* stats-visits-week.json */; }; + 4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977A221F153A00A298E4 /* stats-visits-day.json */; }; + 4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977D221F269A00A298E4 /* stats-visits-month.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 +521,10 @@ 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 = ""; }; + 40819777221F00E600A298E4 /* SummaryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryStatsType.swift; sourceTree = ""; }; + 40819779221F153A00A298E4 /* stats-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-week.json"; sourceTree = ""; }; + 4081977A221F153A00A298E4 /* stats-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-day.json"; sourceTree = ""; }; + 4081977D221F269A00A298E4 /* stats-visits-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-month.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 +1030,7 @@ 404057D1221C56AB0060250C /* CountryStatsType.swift */, 404057D5221C92660060250C /* ClicksStatsType.swift */, 404057D9221C9D560060250C /* ReferrerStatsType.swift */, + 40819777221F00E600A298E4 /* SummaryStatsType.swift */, ); path = "Time-based data"; sourceTree = ""; @@ -1029,6 +1038,7 @@ 40414061220F9F2800CF7C5B /* Insights */ = { isa = PBXGroup; children = ( + F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */, 40E7FEA8220FA4050032834E /* StatsEmailFollowersInsight.swift */, 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */, @@ -1047,7 +1057,6 @@ children = ( 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, - F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, ); name = V2; sourceTree = ""; @@ -1540,6 +1549,9 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 4081977A221F153A00A298E4 /* stats-visits-day.json */, + 4081977D221F269A00A298E4 /* stats-visits-month.json */, + 40819779221F153A00A298E4 /* stats-visits-week.json */, 404057DB221C9FD70060250C /* stats-referrer-data.json */, 404057D7221C98690060250C /* stats-clicks-data.json */, 404057D3221C5FC30060250C /* stats-countries-data.json */, @@ -2102,6 +2114,7 @@ 74B040721EF8B366002C6258 /* rest-site-settings.json in Resources */, 93BD27611EE73442002BB00B /* me-sites-empty-success.json in Resources */, 40E4698D2017D2E30030DB5F /* plugin-directory-new.json in Resources */, + 4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */, 74FC6F431F191C1D00112505 /* notifications-load-all.json in Resources */, 74C473C11EF32C74009918F2 /* site-export-missing-status-failure.json in Resources */, 828A2400201B671F004F6859 /* activity-restore-success.json in Resources */, @@ -2148,6 +2161,7 @@ 74C473BB1EF328D8009918F2 /* site-export-success.json in Resources */, 7403A2FE1EF06FEB00DED7DC /* me-settings-change-web-address-success.json in Resources */, 93BD27621EE73442002BB00B /* me-sites-success.json in Resources */, + 4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */, 826016FB1F9FAF6300533B6C /* activity-log-auth-failure.json in Resources */, 73D592FB21E550D300E4CF84 /* site-verticals-multiple.json in Resources */, 7403A2F61EF06FEB00DED7DC /* me-settings-change-aboutme-success.json in Resources */, @@ -2170,6 +2184,7 @@ 93AC8ECB1ED32FD000900F5A /* stats-v1.1-followers-email-day.json in Resources */, 740B23E41F17FB4200067A2A /* xmlrpc-metaweblog-editpost-success.xml in Resources */, 7434E1DE1F17C3C900C40DDB /* site-users-update-role-unknown-user-failure.json in Resources */, + 4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2338,6 +2353,7 @@ 9368C7B81EC630270092CE8E /* StatsStreakItem.m in Sources */, 404057C9221B789B0060250C /* AuthorsStatsType.swift in Sources */, 74BA04F61F06DC0A00ED5CD8 /* CommentServiceRemoteXMLRPC.m in Sources */, + 40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */, 7430C9A81F1927180051B8E6 /* ReaderTopicServiceRemote.m in Sources */, 17CE77F120C6EB41001DEA5A /* ReaderFeed.swift in Sources */, 93C674F21EE8351E00BFAF05 /* NSMutableDictionary+Helpers.m in Sources */, diff --git a/WordPressKit/StatsLastPostInsight.swift b/WordPressKit/Insights/StatsLastPostInsight.swift similarity index 100% rename from WordPressKit/StatsLastPostInsight.swift rename to WordPressKit/Insights/StatsLastPostInsight.swift diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 1d06aff1..d04bb0f6 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -65,9 +65,15 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { let pathComponent = TimeStatsType.pathComponent let path = self.path(forEndpoint: "sites/\(siteID)/\(pathComponent)/", withVersion: ._1_1) - let properties = ["period": period.stringValue, - "date": periodDataQueryDateFormatter.string(from: endingOn), - "max": limit as AnyObject] as [String: AnyObject] + let staticProperties = ["period": period.stringValue, + "date": periodDataQueryDateFormatter.string(from: endingOn), + "max": limit as AnyObject] as [String: AnyObject] + + let classProperties = TimeStatsType.queryProperties as [String: AnyObject] + + let properties = staticProperties.merging(classProperties) { val1, _ in + return val1 + } wordPressComRestApi.GET(path, parameters: properties, success: { (response, _) in guard @@ -174,6 +180,7 @@ public protocol InsightProtocol { // naming is hard. public protocol TimeStatsProtocol { static var pathComponent: String { get } + static var queryProperties: [String: String] { get } var period: StatsPeriodUnit { get } var periodEndDate: Date { get } @@ -183,6 +190,10 @@ public protocol TimeStatsProtocol { extension TimeStatsProtocol { + public static var queryProperties: [String: String] { + return [:] + } + // Most of the responses for time data come in a unwieldy format, that requires awkwkard unwrapping // at the call-site — unfortunately not _all of them_, which means we can't just do it at the request level. static func unwrapDaysDictionary(jsonDictionary: [String: AnyObject]) -> [String: AnyObject]? { diff --git a/WordPressKit/Time-based data/SummaryStatsType.swift b/WordPressKit/Time-based data/SummaryStatsType.swift new file mode 100644 index 00000000..f68b30ef --- /dev/null +++ b/WordPressKit/Time-based data/SummaryStatsType.swift @@ -0,0 +1,126 @@ +public struct SummaryStatsType { + public let period: StatsPeriodUnit + public let periodEndDate: Date + + public let summaryData: [StatsSummaryData] +} + +public struct StatsSummaryData { + public let period: StatsPeriodUnit + public let periodStartDate: Date + + public let viewsCount: Int + public let visitorsCount: Int + public let likesCount: Int + public let commentsCount: Int +} + +extension SummaryStatsType: TimeStatsProtocol { + public static var pathComponent: String { + return "stats/visits" + } + + public static var queryProperties: [String: String] { + return ["quantity": "10", + "stat_fields": "views,visitors,likes,comments"] + } + + public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { + guard + let fieldsArray = jsonDictionary["fields"] as? [String], + let data = jsonDictionary["data"] as? [[Any]] + else { + return nil + } + + // The shape of data for this response is somewhat unconventional. + // (you might want to take a peek at included tests fixtures files `stats-visits-*.json`) + // There's a `fields` arrray with strings that correspond to requested properties + // (e.g. something like ["period", "views", "visitors"]. + // The actual data we're after is then contained in the `data`... array of arrays? + // The "inner" arrays contain multiple entries, whose indexes correspond to + // the positions of the appropriate keys in the `fields` array, so in our example the array looks something like this: + // [["2019-01-01", 9001, 1234], ["2019-02-01", 1234, 1234]], where the first object in the "inner" array + // is the `period`, second is `views`, etc. + + guard + let periodIndex = fieldsArray.firstIndex(of: "period"), + let viewsIndex = fieldsArray.firstIndex(of: "views"), + let visitorsIndex = fieldsArray.firstIndex(of: "visitors"), + let likesIndex = fieldsArray.firstIndex(of: "likes"), + let commentsIndex = fieldsArray.firstIndex(of: "comments") + else { + return nil + } + + self.period = period + self.periodEndDate = date + self.summaryData = data.compactMap { StatsSummaryData(dataArray: $0, + period: period, + periodIndex: periodIndex, + viewsIndex: viewsIndex, + visitorsIndex: visitorsIndex, + likesIndex: likesIndex, + commentsIndex: commentsIndex) } + } +} + +private extension StatsSummaryData { + init?(dataArray: [Any], + period: StatsPeriodUnit, + periodIndex: Int, + viewsIndex: Int, + visitorsIndex: Int, + likesIndex: Int, + commentsIndex: Int) { + guard + let periodString = dataArray[periodIndex] as? String, + let periodStart = type(of: self).parsedDate(from: periodString, for: period), + let viewsCount = dataArray[viewsIndex] as? Int, + let visitorsCount = dataArray[visitorsIndex] as? Int, + let likesCount = dataArray[likesIndex] as? Int, + let commentsCount = dataArray[commentsIndex] as? Int + else { + return nil + } + + self.period = period + self.periodStartDate = periodStart + self.viewsCount = viewsCount + self.visitorsCount = visitorsCount + self.likesCount = likesCount + self.commentsCount = commentsCount + } + + static func parsedDate(from dateString: String, for period: StatsPeriodUnit) -> Date? { + switch period { + case .week: + return self.weeksDateFormatter.date(from: dateString) + case .day, .month, .year: + return self.regularDateFormatter.date(from: dateString) + } + } + + static var regularDateFormatter: DateFormatter { + let df = DateFormatter() + df.locale = Locale(identifier: "en_US_POS") + df.dateFormat = "yyyy-MM-dd" + return df + } + + // We have our own handrolled date format for data broken up on week basis. + // Example dates in this format are `2019W02W18` or `2019W02W11`. + // The structure is rougly `aaaaWbbWcc`, where: + // - `aaaa` is four-digit year number, + // - `bb` is two-digit month number + // - `cc` is two-digit day number + // Note that in contrast to almost every other date used in Stats, those dates + // represent the _beginning_ of the period they're applying to, e.g. + // data set for `2019W02W18` is containing data for the period of Feb 18 - Feb 24 2019. + private static var weeksDateFormatter: DateFormatter { + let df = DateFormatter() + df.locale = Locale(identifier: "en_US_POS") + df.dateFormat = "yyyy'W'MM'W'dd" + return df + } +} diff --git a/WordPressKitTests/Mock Data/stats-visits-day.json b/WordPressKitTests/Mock Data/stats-visits-day.json new file mode 100644 index 00000000..574343ff --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-visits-day.json @@ -0,0 +1,105 @@ +{ + "date": "2019-02-21", + "unit": "day", + "fields": [ + "period", + "views", + "visitors", + "likes", + "reblogs", + "comments", + "posts" + ], + "data": [ + [ + "2019-02-12", + 5140, + 3560, + 70, + 0, + 1, + 0 + ], + [ + "2019-02-13", + 4731, + 3351, + 60, + 0, + 0, + 0 + ], + [ + "2019-02-14", + 4644, + 3136, + 50, + 0, + 1, + 0 + ], + [ + "2019-02-15", + 4497, + 2952, + 42, + 0, + 0, + 0 + ], + [ + "2019-02-16", + 3584, + 2471, + 53, + 0, + 0, + 0 + ], + [ + "2019-02-17", + 3604, + 2589, + 47, + 0, + 0, + 0 + ], + [ + "2019-02-18", + 4561, + 3193, + 32, + 0, + 0, + 0 + ], + [ + "2019-02-19", + 4799, + 3361, + 36, + 0, + 0, + 0 + ], + [ + "2019-02-20", + 4563, + 3187, + 33, + 0, + 0, + 0 + ], + [ + "2019-02-21", + 3244, + 2127, + 25, + 0, + 0, + 0 + ] + ] +} diff --git a/WordPressKitTests/Mock Data/stats-visits-month.json b/WordPressKitTests/Mock Data/stats-visits-month.json new file mode 100644 index 00000000..2e6749c1 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-visits-month.json @@ -0,0 +1,83 @@ +{ + "date": "2019-02-21", + "unit": "month", + "fields": [ + "period", + "views", + "visitors", + "comments", + "likes" + ], + "data": [ + [ + "2018-05-01", + 3496, + 398, + 0, + 72 + ], + [ + "2018-06-01", + 3361, + 417, + 0, + 52 + ], + [ + "2018-07-01", + 3537, + 461, + 0, + 71 + ], + [ + "2018-08-01", + 3097, + 526, + 0, + 68 + ], + [ + "2018-09-01", + 2851, + 534, + 0, + 75 + ], + [ + "2018-10-01", + 4144, + 669, + 0, + 149 + ], + [ + "2018-11-01", + 4579, + 884, + 0, + 162 + ], + [ + "2018-12-01", + 2674, + 460, + 0, + 106 + ], + [ + "2019-01-01", + 3499, + 584, + 0, + 114 + ], + [ + "2019-02-01", + 2569, + 334, + 0, + 116 + ] + ] +} diff --git a/WordPressKitTests/Mock Data/stats-visits-week.json b/WordPressKitTests/Mock Data/stats-visits-week.json new file mode 100644 index 00000000..7c8bb237 --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-visits-week.json @@ -0,0 +1,105 @@ +{ + "date": "2019-02-21", + "unit": "week", + "fields": [ + "period", + "views", + "visitors", + "likes", + "reblogs", + "comments", + "posts" + ], + "data": [ + [ + "2018W12W17", + 32603, + 23205, + 855, + 0, + 44, + 1 + ], + [ + "2018W12W24", + 26390, + 18227, + 284, + 0, + 1, + 0 + ], + [ + "2018W12W31", + 38403, + 25438, + 1280, + 0, + 28, + 1 + ], + [ + "2019W01W07", + 39187, + 27373, + 1479, + 0, + 115, + 1 + ], + [ + "2019W01W14", + 50722, + 36047, + 1043, + 0, + 6, + 1 + ], + [ + "2019W01W21", + 31502, + 21832, + 328, + 0, + 0, + 0 + ], + [ + "2019W01W28", + 31021, + 20977, + 339, + 0, + 0, + 0 + ], + [ + "2019W02W04", + 34140, + 23003, + 779, + 0, + 21, + 1 + ], + [ + "2019W02W11", + 31338, + 20752, + 403, + 0, + 2, + 0 + ], + [ + "2019W02W18", + 17162, + 11490, + 126, + 0, + 0, + 0 + ] + ] +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index 9a9f6ce6..39ad61a3 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -15,7 +15,10 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getCountriesMockFilename = "stats-countries-data.json" let getClicksMockFilename = "stats-clicks-data.json" let getReferrersMockFilename = "stats-referrer-data.json" - + let getVisitsDayMockFilename = "stats-visits-day.json" + let getVisitsWeekMockFilename = "stats-visits-week.json" + let getVisitsMonthMockFilename = "stats-visits-month.json" + // MARK: - Properties var siteStreakEndpoint: String { return "sites/\(siteID)/stats/streak" } @@ -25,6 +28,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 siteVisitsDataEndpoint: String { return "sites/\(siteID)/stats/visits/" } var remote: StatsServiceRemoteV2! @@ -288,4 +292,115 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) } + + func testVisitsForDay() { + let expect = expectation(description: "It should return visits data for a day") + + stubRemoteResponse(siteVisitsDataEndpoint, filename: getVisitsDayMockFilename, contentType: .ApplicationJSON) + + let feb21 = DateComponents(year: 2019, month: 2, day: 21) + let date = Calendar.autoupdatingCurrent.date(from: feb21)! + + remote.getData(for: .day, endingOn: date) { (summary: SummaryStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(summary) + + XCTAssertEqual(summary?.summaryData.count, 10) + + XCTAssertEqual(summary?.summaryData[0].viewsCount, 5140) + XCTAssertEqual(summary?.summaryData[0].visitorsCount, 3560) + XCTAssertEqual(summary?.summaryData[0].likesCount, 70) + XCTAssertEqual(summary?.summaryData[0].commentsCount, 1) + + let nineDaysAgo = Calendar.autoupdatingCurrent.date(byAdding: .day, value: -9, to: date)! + XCTAssertEqual(summary?.summaryData[0].periodStartDate, nineDaysAgo) + + XCTAssertEqual(summary?.summaryData[9].viewsCount, 3244) + XCTAssertEqual(summary?.summaryData[9].visitorsCount, 2127) + XCTAssertEqual(summary?.summaryData[9].likesCount, 25) + XCTAssertEqual(summary?.summaryData[9].commentsCount, 0) + XCTAssertEqual(summary?.summaryData[9].periodStartDate, date) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + + } + + func testVisitsForWeek() { + let expect = expectation(description: "It should return visits data for a week") + + stubRemoteResponse(siteVisitsDataEndpoint, filename: getVisitsWeekMockFilename, contentType: .ApplicationJSON) + + let feb21 = DateComponents(year: 2019, month: 2, day: 21) + let date = Calendar.autoupdatingCurrent.date(from: feb21)! + + remote.getData(for: .week, endingOn: date) { (summary: SummaryStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(summary) + + XCTAssertEqual(summary?.summaryData.count, 10) + + XCTAssertEqual(summary?.summaryData[0].viewsCount, 32603) + XCTAssertEqual(summary?.summaryData[0].visitorsCount, 23205) + XCTAssertEqual(summary?.summaryData[0].likesCount, 855) + XCTAssertEqual(summary?.summaryData[0].commentsCount, 44) + + let dec17 = DateComponents(year: 2018, month: 12, day: 17) + let dec17Date = Calendar.autoupdatingCurrent.date(from: dec17)! + XCTAssertEqual(summary?.summaryData[0].periodStartDate, dec17Date) + + XCTAssertEqual(summary?.summaryData[9].viewsCount, 17162) + XCTAssertEqual(summary?.summaryData[9].visitorsCount, 11490) + XCTAssertEqual(summary?.summaryData[9].likesCount, 126) + XCTAssertEqual(summary?.summaryData[9].commentsCount, 0) + + XCTAssertEqual(summary?.summaryData[9].periodStartDate, Calendar.autoupdatingCurrent.date(byAdding: .day, + value: 7 * 9, // 7 days * nine objects + to: dec17Date)) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } + + func testVisitsForMonth() { + let expect = expectation(description: "It should return visits data for a month") + + stubRemoteResponse(siteVisitsDataEndpoint, filename: getVisitsMonthMockFilename, contentType: .ApplicationJSON) + + let feb21 = DateComponents(year: 2019, month: 2, day: 21) + let date = Calendar.autoupdatingCurrent.date(from: feb21)! + + remote.getData(for: .month, endingOn: date) { (summary: SummaryStatsType?, error: Error?) in + XCTAssertNil(error) + XCTAssertNotNil(summary) + + XCTAssertEqual(summary?.summaryData.count, 10) + + XCTAssertEqual(summary?.summaryData[0].viewsCount, 3496) + XCTAssertEqual(summary?.summaryData[0].visitorsCount, 398) + XCTAssertEqual(summary?.summaryData[0].likesCount, 72) + XCTAssertEqual(summary?.summaryData[0].commentsCount, 0) + + let may1 = DateComponents(year: 2018, month: 5, day: 1) + let may1Date = Calendar.autoupdatingCurrent.date(from: may1)! + XCTAssertEqual(summary?.summaryData[0].periodStartDate, may1Date) + + XCTAssertEqual(summary?.summaryData[9].viewsCount, 2569) + XCTAssertEqual(summary?.summaryData[9].visitorsCount, 334) + XCTAssertEqual(summary?.summaryData[9].likesCount, 116) + XCTAssertEqual(summary?.summaryData[9].commentsCount, 0) + + let nineMonthsFromMay1 = Calendar.autoupdatingCurrent.date(byAdding: .month, value: 9, to: may1Date)! + + XCTAssertEqual(summary?.summaryData[9].periodStartDate, nineMonthsFromMay1) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } } From 8ec303737b5bb315525839a3776fb426108d7bfd Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 20:53:12 +0100 Subject: [PATCH 20/27] Fix queries for the SummaryStatsType --- WordPressKit/StatsServiceRemoteV2.swift | 9 +++++---- WordPressKit/Time-based data/SummaryStatsType.swift | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index d04bb0f6..dbb2e4f1 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -69,7 +69,7 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { "date": periodDataQueryDateFormatter.string(from: endingOn), "max": limit as AnyObject] as [String: AnyObject] - let classProperties = TimeStatsType.queryProperties as [String: AnyObject] + let classProperties = TimeStatsType.queryProperties(with: endingOn, period: period) as [String: AnyObject] let properties = staticProperties.merging(classProperties) { val1, _ in return val1 @@ -180,17 +180,18 @@ public protocol InsightProtocol { // naming is hard. public protocol TimeStatsProtocol { static var pathComponent: String { get } - static var queryProperties: [String: String] { get } var period: StatsPeriodUnit { get } var periodEndDate: Date { get } init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String: AnyObject]) + + static func queryProperties(with date: Date, period: StatsPeriodUnit) -> [String: String] } extension TimeStatsProtocol { - public static var queryProperties: [String: String] { + public static func queryProperties(with date: Date, period: StatsPeriodUnit) -> [String: String] { return [:] } @@ -211,7 +212,7 @@ extension TimeStatsProtocol { // We'll bring `StatsPeriodUnit` into this file when the "old" `WPStatsServiceRemote` gets removed. // For now we can piggy-back off the old type and add this as an extension. -fileprivate extension StatsPeriodUnit { +extension StatsPeriodUnit { var stringValue: String { switch self { case .day: diff --git a/WordPressKit/Time-based data/SummaryStatsType.swift b/WordPressKit/Time-based data/SummaryStatsType.swift index f68b30ef..aefac767 100644 --- a/WordPressKit/Time-based data/SummaryStatsType.swift +++ b/WordPressKit/Time-based data/SummaryStatsType.swift @@ -20,9 +20,10 @@ extension SummaryStatsType: TimeStatsProtocol { return "stats/visits" } - public static var queryProperties: [String: String] { + public static func queryProperties(with date: Date, period: StatsPeriodUnit) -> [String: String] { return ["quantity": "10", - "stat_fields": "views,visitors,likes,comments"] + "stat_fields": "views,visitors,likes,comments", + "unit": period.stringValue] } public init?(date: Date, period: StatsPeriodUnit, jsonDictionary: [String : AnyObject]) { From 0683f28f01ec40cd0667ed6cba50a21100f7b6a7 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 21:11:16 +0100 Subject: [PATCH 21/27] 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, From 263f75a1f4399cd0af4d074f222e0dad3aff837e Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 21:14:30 +0100 Subject: [PATCH 22/27] omit pointless `?` --- WordPressKit/Time-based data/AuthorsStatsType.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressKit/Time-based data/AuthorsStatsType.swift b/WordPressKit/Time-based data/AuthorsStatsType.swift index e0ac1d06..525b58dd 100644 --- a/WordPressKit/Time-based data/AuthorsStatsType.swift +++ b/WordPressKit/Time-based data/AuthorsStatsType.swift @@ -99,11 +99,11 @@ extension StatsTopPost { static func kind(from kindString: String?) -> Kind { switch kindString { - case "post"?: + case "post": return .post - case "homepage"?: + case "homepage": return .homepage - case "page"?: + case "page": return .page default: return .unknown From 9e3935db5fa15f395e5c2fe10d7717f9f37f71eb Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Thu, 21 Feb 2019 22:45:26 +0100 Subject: [PATCH 23/27] unbreak build after bad merge --- WordPressKit/Time-based data/PublishedPostsStatsType.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WordPressKit/Time-based data/PublishedPostsStatsType.swift b/WordPressKit/Time-based data/PublishedPostsStatsType.swift index 7283a1b8..a693f9c5 100644 --- a/WordPressKit/Time-based data/PublishedPostsStatsType.swift +++ b/WordPressKit/Time-based data/PublishedPostsStatsType.swift @@ -35,5 +35,6 @@ private extension StatsTopPost { self.title = title self.postURL = URL(string: urlString) self.viewsCount = 0 + self.kind = .unknown } } From fe9e21f8fd4da82d5f3a3f27a854723b97c20832 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Fri, 22 Feb 2019 01:27:25 +0100 Subject: [PATCH 24/27] Add support for fetching detail stats for specific post --- WordPressKit.xcodeproj/project.pbxproj | 9 +- WordPressKit/StatsPostDetails.swift | 148 + WordPressKit/StatsServiceRemoteV2.swift | 18 + .../Mock Data/stats-post-details.json | 5428 +++++++++++++++++ WordPressKitTests/StatsRemoteV2Tests.swift | 78 +- 5 files changed, 5679 insertions(+), 2 deletions(-) create mode 100644 WordPressKit/StatsPostDetails.swift create mode 100644 WordPressKitTests/Mock Data/stats-post-details.json diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index d4b3de56..e2ca09db 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -36,6 +36,8 @@ 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819770221DFDB600A298E4 /* stats-posts-data.json */; }; 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 */; }; + 40819783221F5C8200A298E4 /* StatsPostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819782221F5C8200A298E4 /* StatsPostDetails.swift */; }; + 40819785221F74B200A298E4 /* stats-post-details.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819784221F74B200A298E4 /* stats-post-details.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 */; }; @@ -525,7 +527,8 @@ 40819770221DFDB600A298E4 /* stats-posts-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-posts-data.json"; 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 = ""; }; - + 40819782221F5C8200A298E4 /* StatsPostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPostDetails.swift; sourceTree = ""; }; + 40819784221F74B200A298E4 /* stats-post-details.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-post-details.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 = ""; }; @@ -1059,6 +1062,7 @@ 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, + 40819782221F5C8200A298E4 /* StatsPostDetails.swift */, ); name = V2; sourceTree = ""; @@ -1551,6 +1555,7 @@ 93BD27421EE73384002BB00B /* Mock Data */ = { isa = PBXGroup; children = ( + 40819784221F74B200A298E4 /* stats-post-details.json */, 40819770221DFDB600A298E4 /* stats-posts-data.json */, 40819774221E497C00A298E4 /* stats-published-posts.json */, 404057DB221C9FD70060250C /* stats-referrer-data.json */, @@ -2006,6 +2011,7 @@ 93BD275C1EE73442002BB00B /* is-available-username-success.json in Resources */, 93AC8ED81ED32FD000900F5A /* stats-v1.1-top-posts-day.json in Resources */, 74D67F331F15C3740010C5ED /* site-users-delete-auth-failure.json in Resources */, + 40819785221F74B200A298E4 /* stats-post-details.json in Resources */, 436D563A2118DE3B00CEAA33 /* supported-countries-success.json in Resources */, 740B23E71F17FB4200067A2A /* xmlrpc-metaweblog-newpost-success.xml in Resources */, 74FC6F411F191C1D00112505 /* notifications-last-seen.json in Resources */, @@ -2347,6 +2353,7 @@ 93BD27831EE73944002BB00B /* WordPressRSDParser.swift in Sources */, 7328420421CD786C00126755 /* WordPressComServiceRemote+SiteCreation.swift in Sources */, 826016F31F9FA17B00533B6C /* Activity.swift in Sources */, + 40819783221F5C8200A298E4 /* StatsPostDetails.swift in Sources */, 7397F01A220A072500C723F3 /* ActivityServiceRemote_ApiVersion1_0.swift in Sources */, 40E7FEB1220FB3B60032834E /* StatsAnnualAndMostPopularTimeInsight.swift in Sources */, 7E3E7A4A20E443890075D159 /* Scanner+extensions.swift in Sources */, diff --git a/WordPressKit/StatsPostDetails.swift b/WordPressKit/StatsPostDetails.swift new file mode 100644 index 00000000..ff193ab3 --- /dev/null +++ b/WordPressKit/StatsPostDetails.swift @@ -0,0 +1,148 @@ +public struct StatsPostDetails { + public let fetchedDate: Date + public let totalViewsCount: Int + + public let recentWeeks: [StatsWeeklyBreakdown] + public let dailyAveragesPerMonth: [StatsPostViews] + public let monthlyBreakdown: [StatsPostViews] + public let lastTwoWeeks: [StatsPostViews] +} + +public struct StatsWeeklyBreakdown { + public let startDay: DateComponents + public let endDay: DateComponents + + public let totalViewsCount: Int + public let averageViewsCount: Int + public let changePercentage: Double + + public let days: [StatsPostViews] +} + +public struct StatsPostViews { + public let period: StatsPeriodUnit + public let date: DateComponents + public let viewsCount: Int +} + +extension StatsPostDetails { + init?(jsonDictionary: [String: AnyObject]) { + guard + let fetchedDateString = jsonDictionary["date"] as? String, + let date = type(of: self).dateFormatter.date(from: fetchedDateString), + let totalViewsCount = jsonDictionary["views"] as? Int, + let monthlyBreakdown = jsonDictionary["years"] as? [String: AnyObject], + let monthlyAverages = jsonDictionary["averages"] as? [String: AnyObject], + let recentWeeks = jsonDictionary["weeks"] as? [[String: AnyObject]], + let data = jsonDictionary["data"] as? [[Any]] + else { + return nil + } + + self.fetchedDate = date + self.totalViewsCount = totalViewsCount + + // It's very hard to describe the format of this response. I tried to make the parsing + // as nice and readable as possible, but in all honestly it's still pretty nasty. + // If you want to see an example response to see how weird this response is, check out + // `stats-post-details.json`. + self.recentWeeks = StatsPostViews.mapWeeklyBreakdown(jsonDictionary: recentWeeks) + self.monthlyBreakdown = StatsPostViews.mapMonthlyBreakdown(jsonDictionary: monthlyBreakdown) + self.dailyAveragesPerMonth = StatsPostViews.mapMonthlyBreakdown(jsonDictionary: monthlyAverages) + self.lastTwoWeeks = StatsPostViews.mapDailyData(data: Array(data.suffix(14))) + } + + static var dateFormatter: DateFormatter { + let df = DateFormatter() + df.locale = Locale(identifier: "en_US_POS") + df.dateFormat = "yyyy-MM-dd" + return df + } +} + +extension StatsPostViews { + static func mapMonthlyBreakdown(jsonDictionary: [String: AnyObject]) -> [StatsPostViews] { + return jsonDictionary.flatMap { yearKey, value -> [StatsPostViews] in + guard + let yearInt = Int(yearKey), + let monthsDict = value as? [String: AnyObject], + let months = monthsDict["months"] as? [String: Int] + else { + return [] + } + + return months.compactMap { monthKey, value in + guard + let month = Int(monthKey) + else { + return nil + } + + return StatsPostViews(period: .month, + date: DateComponents(year: yearInt, month: month), + viewsCount: value) + } + } + } +} + +extension StatsPostViews { + static func mapWeeklyBreakdown(jsonDictionary: [[String: AnyObject]]) -> [StatsWeeklyBreakdown] { + return jsonDictionary.compactMap { + guard + let totalViews = $0["total"] as? Int, + let averageViews = $0["average"] as? Int, + let days = $0["days"] as? [[String: AnyObject]] + else { + return nil + } + + let change = ($0["change"] as? Double) ?? 0.0 + + let mappedDays: [StatsPostViews] = days.compactMap { + guard + let dayString = $0["day"] as? String, + let date = StatsPostDetails.dateFormatter.date(from: dayString), + let viewsCount = $0["count"] as? Int + else { + return nil + } + + return StatsPostViews(period: .day, + date: Calendar.autoupdatingCurrent.dateComponents([.year, .month, .day], from: date), + viewsCount: viewsCount) + } + + guard !mappedDays.isEmpty else { + return nil + } + + + return StatsWeeklyBreakdown(startDay: mappedDays.first!.date, + endDay: mappedDays.last!.date, + totalViewsCount: totalViews, + averageViewsCount: averageViews, + changePercentage: change, + days: mappedDays) + } + + } +} + +extension StatsPostViews { + static func mapDailyData(data: [[Any]]) -> [StatsPostViews] { + return data.compactMap { + guard + let dateString = $0[0] as? String, + let date = StatsPostDetails.dateFormatter.date(from: dateString), + let viewsCount = $0[1] as? Int + else { + return nil + } + + return StatsPostViews(period: .day, + date: Calendar.autoupdatingCurrent.dateComponents([.year, .month, .day], from: date), + viewsCount: viewsCount) + } + } +} diff --git a/WordPressKit/StatsServiceRemoteV2.swift b/WordPressKit/StatsServiceRemoteV2.swift index 9e48c300..77bf7364 100644 --- a/WordPressKit/StatsServiceRemoteV2.swift +++ b/WordPressKit/StatsServiceRemoteV2.swift @@ -97,6 +97,24 @@ public class StatsServiceRemoteV2: ServiceRemoteWordPressComREST { completion(nil, error) }) } + + public func getDetails(forPostID postID: Int, completion: @escaping ((StatsPostDetails?, Error?) -> Void)) { + let path = self.path(forEndpoint: "sites/\(siteID)/post/\(postID)/", withVersion: ._1_1) + + wordPressComRestApi.GET(path, parameters: [:], success: { (response, _) in + guard + let jsonResponse = response as? [String: AnyObject], + let postDetails = StatsPostDetails(jsonDictionary: jsonResponse) + else { + completion(nil, ResponseError.decodingFailure) + return + } + + completion(postDetails, nil) + }, failure: { (error, _) in + completion(nil, error) + }) + } } // MARK: - StatsLastPostInsight-specific hack diff --git a/WordPressKitTests/Mock Data/stats-post-details.json b/WordPressKitTests/Mock Data/stats-post-details.json new file mode 100644 index 00000000..de3d3edb --- /dev/null +++ b/WordPressKitTests/Mock Data/stats-post-details.json @@ -0,0 +1,5428 @@ +{ + "date": "2019-02-21", + "views": 163343, + "years": { + "2015": { + "months": { + "3": 1525, + "4": 6966, + "5": 5175, + "6": 3547, + "7": 2457, + "8": 4637, + "9": 6004, + "10": 1212, + "11": 2621, + "12": 3717 + }, + "total": 37861 + }, + "2016": { + "months": { + "1": 4467, + "2": 3255, + "3": 2856, + "4": 2244, + "5": 2837, + "6": 2716, + "7": 3063, + "8": 4111, + "9": 2683, + "10": 3680, + "11": 2242, + "12": 2293 + }, + "total": 36447 + }, + "2017": { + "months": { + "1": 4350, + "2": 2667, + "3": 3383, + "4": 3977, + "5": 2578, + "6": 3454, + "7": 2359, + "8": 2436, + "9": 2120, + "10": 3365, + "11": 3689, + "12": 3151 + }, + "total": 37529 + }, + "2018": { + "months": { + "1": 3491, + "2": 2697, + "3": 8800, + "4": 2702, + "5": 3496, + "6": 3361, + "7": 3537, + "8": 3097, + "9": 2851, + "10": 4144, + "11": 4579, + "12": 2674 + }, + "total": 45429 + }, + "2019": { + "months": { + "1": 3499, + "2": 2578 + }, + "total": 6077 + } + }, + "averages": { + "2015": { + "months": { + "3": 101, + "4": 232, + "5": 166, + "6": 118, + "7": 79, + "8": 149, + "9": 200, + "10": 39, + "11": 87, + "12": 119 + }, + "overall": 130 + }, + "2016": { + "months": { + "1": 144, + "2": 112, + "3": 92, + "4": 74, + "5": 91, + "6": 90, + "7": 98, + "8": 132, + "9": 89, + "10": 118, + "11": 74, + "12": 73 + }, + "overall": 99 + }, + "2017": { + "months": { + "1": 140, + "2": 95, + "3": 109, + "4": 132, + "5": 83, + "6": 115, + "7": 76, + "8": 78, + "9": 70, + "10": 108, + "11": 122, + "12": 101 + }, + "overall": 102 + }, + "2018": { + "months": { + "1": 112, + "2": 96, + "3": 283, + "4": 90, + "5": 112, + "6": 112, + "7": 114, + "8": 99, + "9": 95, + "10": 133, + "11": 152, + "12": 86 + }, + "overall": 124 + }, + "2019": { + "months": { + "1": 112, + "2": 112 + }, + "overall": 112 + } + }, + "weeks": [ + { + "days": [ + { + "day": "2019-01-14", + "count": 174 + }, + { + "day": "2019-01-15", + "count": 167 + }, + { + "day": "2019-01-16", + "count": 48 + }, + { + "day": "2019-01-17", + "count": 177 + }, + { + "day": "2019-01-18", + "count": 43 + }, + { + "day": "2019-01-19", + "count": 19 + }, + { + "day": "2019-01-20", + "count": 60 + } + ], + "total": 688, + "average": 98, + "change": null + }, + { + "days": [ + { + "day": "2019-01-21", + "count": 111 + }, + { + "day": "2019-01-22", + "count": 35 + }, + { + "day": "2019-01-23", + "count": 114 + }, + { + "day": "2019-01-24", + "count": 143 + }, + { + "day": "2019-01-25", + "count": 199 + }, + { + "day": "2019-01-26", + "count": 113 + }, + { + "day": "2019-01-27", + "count": 46 + } + ], + "total": 761, + "average": 108, + "change": 10.610465116279059 + }, + { + "days": [ + { + "day": "2019-01-28", + "count": 276 + }, + { + "day": "2019-01-29", + "count": 74 + }, + { + "day": "2019-01-30", + "count": 45 + }, + { + "day": "2019-01-31", + "count": 334 + }, + { + "day": "2019-02-01", + "count": 193 + }, + { + "day": "2019-02-02", + "count": 25 + }, + { + "day": "2019-02-03", + "count": 62 + } + ], + "total": 1009, + "average": 144, + "change": 32.58869908015768 + }, + { + "days": [ + { + "day": "2019-02-04", + "count": 78 + }, + { + "day": "2019-02-05", + "count": 63 + }, + { + "day": "2019-02-06", + "count": 71 + }, + { + "day": "2019-02-07", + "count": 61 + }, + { + "day": "2019-02-08", + "count": 112 + }, + { + "day": "2019-02-09", + "count": 103 + }, + { + "day": "2019-02-10", + "count": 30 + } + ], + "total": 518, + "average": 74, + "change": -48.66204162537166 + }, + { + "days": [ + { + "day": "2019-02-11", + "count": 88 + }, + { + "day": "2019-02-12", + "count": 149 + }, + { + "day": "2019-02-13", + "count": 57 + }, + { + "day": "2019-02-14", + "count": 143 + }, + { + "day": "2019-02-15", + "count": 271 + }, + { + "day": "2019-02-16", + "count": 152 + }, + { + "day": "2019-02-17", + "count": 53 + } + ], + "total": 913, + "average": 130, + "change": 76.25482625482624 + }, + { + "days": [ + { + "day": "2019-02-18", + "count": 157 + }, + { + "day": "2019-02-19", + "count": 263 + }, + { + "day": "2019-02-20", + "count": 123 + }, + { + "day": "2019-02-21", + "count": 324 + } + ], + "total": 867, + "average": 181, + "change": 38.77327491785326 + } + ], + "fields": [ + "period", + "views" + ], + "data": [ + [ + "2015-09-05", + 1 + ], + [ + "2015-09-06", + 14 + ], + [ + "2015-09-07", + 99 + ], + [ + "2015-09-08", + 95 + ], + [ + "2015-09-09", + 4 + ], + [ + "2015-09-10", + 13 + ], + [ + "2015-09-11", + 372 + ], + [ + "2015-09-12", + 1128 + ], + [ + "2015-09-13", + 702 + ], + [ + "2015-09-14", + 709 + ], + [ + "2015-09-15", + 463 + ], + [ + "2015-09-16", + 264 + ], + [ + "2015-09-17", + 261 + ], + [ + "2015-09-18", + 300 + ], + [ + "2015-09-19", + 143 + ], + [ + "2015-09-20", + 81 + ], + [ + "2015-09-21", + 151 + ], + [ + "2015-09-22", + 221 + ], + [ + "2015-09-23", + 145 + ], + [ + "2015-09-24", + 106 + ], + [ + "2015-09-25", + 158 + ], + [ + "2015-09-26", + 84 + ], + [ + "2015-09-27", + 46 + ], + [ + "2015-09-28", + 107 + ], + [ + "2015-09-29", + 23 + ], + [ + "2015-09-30", + 104 + ], + [ + "2015-10-01", + 104 + ], + [ + "2015-10-02", + 13 + ], + [ + "2015-10-03", + 18 + ], + [ + "2015-10-04", + 22 + ], + [ + "2015-10-05", + 74 + ], + [ + "2015-10-06", + 68 + ], + [ + "2015-10-07", + 42 + ], + [ + "2015-10-08", + 7 + ], + [ + "2015-10-09", + 8 + ], + [ + "2015-10-10", + 65 + ], + [ + "2015-10-11", + 31 + ], + [ + "2015-10-12", + 28 + ], + [ + "2015-10-13", + 36 + ], + [ + "2015-10-14", + 18 + ], + [ + "2015-10-15", + 31 + ], + [ + "2015-10-16", + 15 + ], + [ + "2015-10-17", + 13 + ], + [ + "2015-10-18", + 7 + ], + [ + "2015-10-19", + 45 + ], + [ + "2015-10-20", + 77 + ], + [ + "2015-10-21", + 52 + ], + [ + "2015-10-22", + 33 + ], + [ + "2015-10-23", + 43 + ], + [ + "2015-10-24", + 0 + ], + [ + "2015-10-25", + 9 + ], + [ + "2015-10-26", + 41 + ], + [ + "2015-10-27", + 39 + ], + [ + "2015-10-28", + 40 + ], + [ + "2015-10-29", + 2 + ], + [ + "2015-10-30", + 183 + ], + [ + "2015-10-31", + 48 + ], + [ + "2015-11-01", + 13 + ], + [ + "2015-11-02", + 33 + ], + [ + "2015-11-03", + 26 + ], + [ + "2015-11-04", + 33 + ], + [ + "2015-11-05", + 70 + ], + [ + "2015-11-06", + 72 + ], + [ + "2015-11-07", + 49 + ], + [ + "2015-11-08", + 13 + ], + [ + "2015-11-09", + 26 + ], + [ + "2015-11-10", + 49 + ], + [ + "2015-11-11", + 8 + ], + [ + "2015-11-12", + 61 + ], + [ + "2015-11-13", + 33 + ], + [ + "2015-11-14", + 53 + ], + [ + "2015-11-15", + 69 + ], + [ + "2015-11-16", + 93 + ], + [ + "2015-11-17", + 447 + ], + [ + "2015-11-18", + 296 + ], + [ + "2015-11-19", + 18 + ], + [ + "2015-11-20", + 132 + ], + [ + "2015-11-21", + 52 + ], + [ + "2015-11-22", + 56 + ], + [ + "2015-11-23", + 154 + ], + [ + "2015-11-24", + 177 + ], + [ + "2015-11-25", + 280 + ], + [ + "2015-11-26", + 136 + ], + [ + "2015-11-27", + 74 + ], + [ + "2015-11-28", + 54 + ], + [ + "2015-11-29", + 15 + ], + [ + "2015-11-30", + 29 + ], + [ + "2015-12-01", + 183 + ], + [ + "2015-12-02", + 43 + ], + [ + "2015-12-03", + 48 + ], + [ + "2015-12-04", + 100 + ], + [ + "2015-12-05", + 11 + ], + [ + "2015-12-06", + 97 + ], + [ + "2015-12-07", + 34 + ], + [ + "2015-12-08", + 96 + ], + [ + "2015-12-09", + 68 + ], + [ + "2015-12-10", + 117 + ], + [ + "2015-12-11", + 53 + ], + [ + "2015-12-12", + 75 + ], + [ + "2015-12-13", + 19 + ], + [ + "2015-12-14", + 18 + ], + [ + "2015-12-15", + 268 + ], + [ + "2015-12-16", + 91 + ], + [ + "2015-12-17", + 1211 + ], + [ + "2015-12-18", + 315 + ], + [ + "2015-12-19", + 30 + ], + [ + "2015-12-20", + 30 + ], + [ + "2015-12-21", + 74 + ], + [ + "2015-12-22", + 212 + ], + [ + "2015-12-23", + 83 + ], + [ + "2015-12-24", + 77 + ], + [ + "2015-12-25", + 2 + ], + [ + "2015-12-26", + 6 + ], + [ + "2015-12-27", + 18 + ], + [ + "2015-12-28", + 67 + ], + [ + "2015-12-29", + 108 + ], + [ + "2015-12-30", + 125 + ], + [ + "2015-12-31", + 38 + ], + [ + "2016-01-01", + 27 + ], + [ + "2016-01-02", + 7 + ], + [ + "2016-01-03", + 57 + ], + [ + "2016-01-04", + 187 + ], + [ + "2016-01-05", + 307 + ], + [ + "2016-01-06", + 301 + ], + [ + "2016-01-07", + 284 + ], + [ + "2016-01-08", + 170 + ], + [ + "2016-01-09", + 50 + ], + [ + "2016-01-10", + 99 + ], + [ + "2016-01-11", + 186 + ], + [ + "2016-01-12", + 198 + ], + [ + "2016-01-13", + 166 + ], + [ + "2016-01-14", + 82 + ], + [ + "2016-01-15", + 59 + ], + [ + "2016-01-16", + 68 + ], + [ + "2016-01-17", + 71 + ], + [ + "2016-01-18", + 150 + ], + [ + "2016-01-19", + 132 + ], + [ + "2016-01-20", + 354 + ], + [ + "2016-01-21", + 184 + ], + [ + "2016-01-22", + 139 + ], + [ + "2016-01-23", + 42 + ], + [ + "2016-01-24", + 114 + ], + [ + "2016-01-25", + 128 + ], + [ + "2016-01-26", + 246 + ], + [ + "2016-01-27", + 214 + ], + [ + "2016-01-28", + 163 + ], + [ + "2016-01-29", + 142 + ], + [ + "2016-01-30", + 103 + ], + [ + "2016-01-31", + 37 + ], + [ + "2016-02-01", + 135 + ], + [ + "2016-02-02", + 232 + ], + [ + "2016-02-03", + 189 + ], + [ + "2016-02-04", + 79 + ], + [ + "2016-02-05", + 80 + ], + [ + "2016-02-06", + 105 + ], + [ + "2016-02-07", + 18 + ], + [ + "2016-02-08", + 93 + ], + [ + "2016-02-09", + 133 + ], + [ + "2016-02-10", + 103 + ], + [ + "2016-02-11", + 138 + ], + [ + "2016-02-12", + 88 + ], + [ + "2016-02-13", + 128 + ], + [ + "2016-02-14", + 56 + ], + [ + "2016-02-15", + 68 + ], + [ + "2016-02-16", + 47 + ], + [ + "2016-02-17", + 110 + ], + [ + "2016-02-18", + 98 + ], + [ + "2016-02-19", + 313 + ], + [ + "2016-02-20", + 125 + ], + [ + "2016-02-21", + 40 + ], + [ + "2016-02-22", + 105 + ], + [ + "2016-02-23", + 33 + ], + [ + "2016-02-24", + 103 + ], + [ + "2016-02-25", + 178 + ], + [ + "2016-02-26", + 192 + ], + [ + "2016-02-27", + 104 + ], + [ + "2016-02-28", + 54 + ], + [ + "2016-02-29", + 108 + ], + [ + "2016-03-01", + 122 + ], + [ + "2016-03-02", + 104 + ], + [ + "2016-03-03", + 240 + ], + [ + "2016-03-04", + 74 + ], + [ + "2016-03-05", + 51 + ], + [ + "2016-03-06", + 27 + ], + [ + "2016-03-07", + 103 + ], + [ + "2016-03-08", + 55 + ], + [ + "2016-03-09", + 79 + ], + [ + "2016-03-10", + 113 + ], + [ + "2016-03-11", + 60 + ], + [ + "2016-03-12", + 8 + ], + [ + "2016-03-13", + 21 + ], + [ + "2016-03-14", + 73 + ], + [ + "2016-03-15", + 91 + ], + [ + "2016-03-16", + 90 + ], + [ + "2016-03-17", + 67 + ], + [ + "2016-03-18", + 107 + ], + [ + "2016-03-19", + 79 + ], + [ + "2016-03-20", + 53 + ], + [ + "2016-03-21", + 129 + ], + [ + "2016-03-22", + 201 + ], + [ + "2016-03-23", + 112 + ], + [ + "2016-03-24", + 92 + ], + [ + "2016-03-25", + 100 + ], + [ + "2016-03-26", + 76 + ], + [ + "2016-03-27", + 25 + ], + [ + "2016-03-28", + 29 + ], + [ + "2016-03-29", + 131 + ], + [ + "2016-03-30", + 158 + ], + [ + "2016-03-31", + 186 + ], + [ + "2016-04-01", + 47 + ], + [ + "2016-04-02", + 53 + ], + [ + "2016-04-03", + 45 + ], + [ + "2016-04-04", + 85 + ], + [ + "2016-04-05", + 93 + ], + [ + "2016-04-06", + 38 + ], + [ + "2016-04-07", + 40 + ], + [ + "2016-04-08", + 60 + ], + [ + "2016-04-09", + 74 + ], + [ + "2016-04-10", + 28 + ], + [ + "2016-04-11", + 119 + ], + [ + "2016-04-12", + 55 + ], + [ + "2016-04-13", + 138 + ], + [ + "2016-04-14", + 45 + ], + [ + "2016-04-15", + 115 + ], + [ + "2016-04-16", + 40 + ], + [ + "2016-04-17", + 7 + ], + [ + "2016-04-18", + 74 + ], + [ + "2016-04-19", + 46 + ], + [ + "2016-04-20", + 156 + ], + [ + "2016-04-21", + 160 + ], + [ + "2016-04-22", + 65 + ], + [ + "2016-04-23", + 4 + ], + [ + "2016-04-24", + 77 + ], + [ + "2016-04-25", + 127 + ], + [ + "2016-04-26", + 64 + ], + [ + "2016-04-27", + 153 + ], + [ + "2016-04-28", + 78 + ], + [ + "2016-04-29", + 43 + ], + [ + "2016-04-30", + 115 + ], + [ + "2016-05-01", + 80 + ], + [ + "2016-05-02", + 110 + ], + [ + "2016-05-03", + 55 + ], + [ + "2016-05-04", + 31 + ], + [ + "2016-05-05", + 22 + ], + [ + "2016-05-06", + 21 + ], + [ + "2016-05-07", + 33 + ], + [ + "2016-05-08", + 47 + ], + [ + "2016-05-09", + 113 + ], + [ + "2016-05-10", + 262 + ], + [ + "2016-05-11", + 43 + ], + [ + "2016-05-12", + 114 + ], + [ + "2016-05-13", + 115 + ], + [ + "2016-05-14", + 102 + ], + [ + "2016-05-15", + 69 + ], + [ + "2016-05-16", + 99 + ], + [ + "2016-05-17", + 318 + ], + [ + "2016-05-18", + 215 + ], + [ + "2016-05-19", + 57 + ], + [ + "2016-05-20", + 108 + ], + [ + "2016-05-21", + 16 + ], + [ + "2016-05-22", + 62 + ], + [ + "2016-05-23", + 71 + ], + [ + "2016-05-24", + 69 + ], + [ + "2016-05-25", + 92 + ], + [ + "2016-05-26", + 149 + ], + [ + "2016-05-27", + 27 + ], + [ + "2016-05-28", + 21 + ], + [ + "2016-05-29", + 71 + ], + [ + "2016-05-30", + 89 + ], + [ + "2016-05-31", + 156 + ], + [ + "2016-06-01", + 32 + ], + [ + "2016-06-02", + 76 + ], + [ + "2016-06-03", + 174 + ], + [ + "2016-06-04", + 50 + ], + [ + "2016-06-05", + 18 + ], + [ + "2016-06-06", + 141 + ], + [ + "2016-06-07", + 70 + ], + [ + "2016-06-08", + 34 + ], + [ + "2016-06-09", + 91 + ], + [ + "2016-06-10", + 88 + ], + [ + "2016-06-11", + 80 + ], + [ + "2016-06-12", + 17 + ], + [ + "2016-06-13", + 67 + ], + [ + "2016-06-14", + 131 + ], + [ + "2016-06-15", + 127 + ], + [ + "2016-06-16", + 50 + ], + [ + "2016-06-17", + 32 + ], + [ + "2016-06-18", + 117 + ], + [ + "2016-06-19", + 49 + ], + [ + "2016-06-20", + 74 + ], + [ + "2016-06-21", + 171 + ], + [ + "2016-06-22", + 292 + ], + [ + "2016-06-23", + 95 + ], + [ + "2016-06-24", + 43 + ], + [ + "2016-06-25", + 4 + ], + [ + "2016-06-26", + 92 + ], + [ + "2016-06-27", + 109 + ], + [ + "2016-06-28", + 143 + ], + [ + "2016-06-29", + 165 + ], + [ + "2016-06-30", + 84 + ], + [ + "2016-07-01", + 96 + ], + [ + "2016-07-02", + 43 + ], + [ + "2016-07-03", + 6 + ], + [ + "2016-07-04", + 38 + ], + [ + "2016-07-05", + 99 + ], + [ + "2016-07-06", + 69 + ], + [ + "2016-07-07", + 70 + ], + [ + "2016-07-08", + 133 + ], + [ + "2016-07-09", + 33 + ], + [ + "2016-07-10", + 70 + ], + [ + "2016-07-11", + 17 + ], + [ + "2016-07-12", + 213 + ], + [ + "2016-07-13", + 104 + ], + [ + "2016-07-14", + 142 + ], + [ + "2016-07-15", + 170 + ], + [ + "2016-07-16", + 17 + ], + [ + "2016-07-17", + 49 + ], + [ + "2016-07-18", + 194 + ], + [ + "2016-07-19", + 171 + ], + [ + "2016-07-20", + 128 + ], + [ + "2016-07-21", + 71 + ], + [ + "2016-07-22", + 62 + ], + [ + "2016-07-23", + 43 + ], + [ + "2016-07-24", + 5 + ], + [ + "2016-07-25", + 207 + ], + [ + "2016-07-26", + 113 + ], + [ + "2016-07-27", + 124 + ], + [ + "2016-07-28", + 270 + ], + [ + "2016-07-29", + 59 + ], + [ + "2016-07-30", + 97 + ], + [ + "2016-07-31", + 150 + ], + [ + "2016-08-01", + 112 + ], + [ + "2016-08-02", + 19 + ], + [ + "2016-08-03", + 145 + ], + [ + "2016-08-04", + 227 + ], + [ + "2016-08-05", + 501 + ], + [ + "2016-08-06", + 71 + ], + [ + "2016-08-07", + 56 + ], + [ + "2016-08-08", + 150 + ], + [ + "2016-08-09", + 122 + ], + [ + "2016-08-10", + 175 + ], + [ + "2016-08-11", + 226 + ], + [ + "2016-08-12", + 30 + ], + [ + "2016-08-13", + 71 + ], + [ + "2016-08-14", + 116 + ], + [ + "2016-08-15", + 33 + ], + [ + "2016-08-16", + 107 + ], + [ + "2016-08-17", + 139 + ], + [ + "2016-08-18", + 129 + ], + [ + "2016-08-19", + 155 + ], + [ + "2016-08-20", + 78 + ], + [ + "2016-08-21", + 65 + ], + [ + "2016-08-22", + 119 + ], + [ + "2016-08-23", + 169 + ], + [ + "2016-08-24", + 186 + ], + [ + "2016-08-25", + 35 + ], + [ + "2016-08-26", + 194 + ], + [ + "2016-08-27", + 133 + ], + [ + "2016-08-28", + 6 + ], + [ + "2016-08-29", + 146 + ], + [ + "2016-08-30", + 164 + ], + [ + "2016-08-31", + 232 + ], + [ + "2016-09-01", + 125 + ], + [ + "2016-09-02", + 109 + ], + [ + "2016-09-03", + 4 + ], + [ + "2016-09-04", + 23 + ], + [ + "2016-09-05", + 234 + ], + [ + "2016-09-06", + 186 + ], + [ + "2016-09-07", + 120 + ], + [ + "2016-09-08", + 98 + ], + [ + "2016-09-09", + 244 + ], + [ + "2016-09-10", + 30 + ], + [ + "2016-09-11", + 37 + ], + [ + "2016-09-12", + 102 + ], + [ + "2016-09-13", + 93 + ], + [ + "2016-09-14", + 58 + ], + [ + "2016-09-15", + 98 + ], + [ + "2016-09-16", + 110 + ], + [ + "2016-09-17", + 33 + ], + [ + "2016-09-18", + 20 + ], + [ + "2016-09-19", + 34 + ], + [ + "2016-09-20", + 67 + ], + [ + "2016-09-21", + 42 + ], + [ + "2016-09-22", + 113 + ], + [ + "2016-09-23", + 40 + ], + [ + "2016-09-24", + 116 + ], + [ + "2016-09-25", + 9 + ], + [ + "2016-09-26", + 95 + ], + [ + "2016-09-27", + 183 + ], + [ + "2016-09-28", + 104 + ], + [ + "2016-09-29", + 93 + ], + [ + "2016-09-30", + 63 + ], + [ + "2016-10-01", + 66 + ], + [ + "2016-10-02", + 80 + ], + [ + "2016-10-03", + 58 + ], + [ + "2016-10-04", + 170 + ], + [ + "2016-10-05", + 112 + ], + [ + "2016-10-06", + 88 + ], + [ + "2016-10-07", + 102 + ], + [ + "2016-10-08", + 159 + ], + [ + "2016-10-09", + 43 + ], + [ + "2016-10-10", + 103 + ], + [ + "2016-10-11", + 125 + ], + [ + "2016-10-12", + 101 + ], + [ + "2016-10-13", + 204 + ], + [ + "2016-10-14", + 121 + ], + [ + "2016-10-15", + 114 + ], + [ + "2016-10-16", + 83 + ], + [ + "2016-10-17", + 102 + ], + [ + "2016-10-18", + 391 + ], + [ + "2016-10-19", + 260 + ], + [ + "2016-10-20", + 69 + ], + [ + "2016-10-21", + 189 + ], + [ + "2016-10-22", + 48 + ], + [ + "2016-10-23", + 49 + ], + [ + "2016-10-24", + 125 + ], + [ + "2016-10-25", + 140 + ], + [ + "2016-10-26", + 122 + ], + [ + "2016-10-27", + 165 + ], + [ + "2016-10-28", + 81 + ], + [ + "2016-10-29", + 29 + ], + [ + "2016-10-30", + 81 + ], + [ + "2016-10-31", + 100 + ], + [ + "2016-11-01", + 121 + ], + [ + "2016-11-02", + 6 + ], + [ + "2016-11-03", + 212 + ], + [ + "2016-11-04", + 94 + ], + [ + "2016-11-05", + 48 + ], + [ + "2016-11-06", + 8 + ], + [ + "2016-11-07", + 93 + ], + [ + "2016-11-08", + 71 + ], + [ + "2016-11-09", + 21 + ], + [ + "2016-11-10", + 61 + ], + [ + "2016-11-11", + 100 + ], + [ + "2016-11-12", + 19 + ], + [ + "2016-11-13", + 77 + ], + [ + "2016-11-14", + 105 + ], + [ + "2016-11-15", + 107 + ], + [ + "2016-11-16", + 82 + ], + [ + "2016-11-17", + 102 + ], + [ + "2016-11-18", + 109 + ], + [ + "2016-11-19", + 84 + ], + [ + "2016-11-20", + 37 + ], + [ + "2016-11-21", + 88 + ], + [ + "2016-11-22", + 16 + ], + [ + "2016-11-23", + 163 + ], + [ + "2016-11-24", + 85 + ], + [ + "2016-11-25", + 78 + ], + [ + "2016-11-26", + 47 + ], + [ + "2016-11-27", + 16 + ], + [ + "2016-11-28", + 108 + ], + [ + "2016-11-29", + 16 + ], + [ + "2016-11-30", + 68 + ], + [ + "2016-12-01", + 30 + ], + [ + "2016-12-02", + 39 + ], + [ + "2016-12-03", + 47 + ], + [ + "2016-12-04", + 81 + ], + [ + "2016-12-05", + 105 + ], + [ + "2016-12-06", + 109 + ], + [ + "2016-12-07", + 77 + ], + [ + "2016-12-08", + 10 + ], + [ + "2016-12-09", + 51 + ], + [ + "2016-12-10", + 112 + ], + [ + "2016-12-11", + 38 + ], + [ + "2016-12-12", + 146 + ], + [ + "2016-12-13", + 96 + ], + [ + "2016-12-14", + 32 + ], + [ + "2016-12-15", + 109 + ], + [ + "2016-12-16", + 59 + ], + [ + "2016-12-17", + 61 + ], + [ + "2016-12-18", + 20 + ], + [ + "2016-12-19", + 286 + ], + [ + "2016-12-20", + 69 + ], + [ + "2016-12-21", + 74 + ], + [ + "2016-12-22", + 131 + ], + [ + "2016-12-23", + 82 + ], + [ + "2016-12-24", + 3 + ], + [ + "2016-12-25", + 44 + ], + [ + "2016-12-26", + 46 + ], + [ + "2016-12-27", + 51 + ], + [ + "2016-12-28", + 114 + ], + [ + "2016-12-29", + 72 + ], + [ + "2016-12-30", + 76 + ], + [ + "2016-12-31", + 23 + ], + [ + "2017-01-01", + 80 + ], + [ + "2017-01-02", + 109 + ], + [ + "2017-01-03", + 238 + ], + [ + "2017-01-04", + 155 + ], + [ + "2017-01-05", + 91 + ], + [ + "2017-01-06", + 36 + ], + [ + "2017-01-07", + 146 + ], + [ + "2017-01-08", + 20 + ], + [ + "2017-01-09", + 120 + ], + [ + "2017-01-10", + 154 + ], + [ + "2017-01-11", + 176 + ], + [ + "2017-01-12", + 70 + ], + [ + "2017-01-13", + 121 + ], + [ + "2017-01-14", + 28 + ], + [ + "2017-01-15", + 106 + ], + [ + "2017-01-16", + 164 + ], + [ + "2017-01-17", + 166 + ], + [ + "2017-01-18", + 205 + ], + [ + "2017-01-19", + 278 + ], + [ + "2017-01-20", + 78 + ], + [ + "2017-01-21", + 32 + ], + [ + "2017-01-22", + 61 + ], + [ + "2017-01-23", + 171 + ], + [ + "2017-01-24", + 241 + ], + [ + "2017-01-25", + 40 + ], + [ + "2017-01-26", + 31 + ], + [ + "2017-01-27", + 92 + ], + [ + "2017-01-28", + 514 + ], + [ + "2017-01-29", + 103 + ], + [ + "2017-01-30", + 196 + ], + [ + "2017-01-31", + 328 + ], + [ + "2017-02-01", + 160 + ], + [ + "2017-02-02", + 165 + ], + [ + "2017-02-03", + 64 + ], + [ + "2017-02-04", + 173 + ], + [ + "2017-02-05", + 83 + ], + [ + "2017-02-06", + 104 + ], + [ + "2017-02-07", + 118 + ], + [ + "2017-02-08", + 55 + ], + [ + "2017-02-09", + 203 + ], + [ + "2017-02-10", + 11 + ], + [ + "2017-02-11", + 14 + ], + [ + "2017-02-12", + 103 + ], + [ + "2017-02-13", + 70 + ], + [ + "2017-02-14", + 145 + ], + [ + "2017-02-15", + 71 + ], + [ + "2017-02-16", + 68 + ], + [ + "2017-02-17", + 26 + ], + [ + "2017-02-18", + 85 + ], + [ + "2017-02-19", + 74 + ], + [ + "2017-02-20", + 37 + ], + [ + "2017-02-21", + 133 + ], + [ + "2017-02-22", + 153 + ], + [ + "2017-02-23", + 146 + ], + [ + "2017-02-24", + 55 + ], + [ + "2017-02-25", + 88 + ], + [ + "2017-02-26", + 30 + ], + [ + "2017-02-27", + 192 + ], + [ + "2017-02-28", + 41 + ], + [ + "2017-03-01", + 223 + ], + [ + "2017-03-02", + 187 + ], + [ + "2017-03-03", + 38 + ], + [ + "2017-03-04", + 31 + ], + [ + "2017-03-05", + 41 + ], + [ + "2017-03-06", + 40 + ], + [ + "2017-03-07", + 69 + ], + [ + "2017-03-08", + 228 + ], + [ + "2017-03-09", + 100 + ], + [ + "2017-03-10", + 113 + ], + [ + "2017-03-11", + 48 + ], + [ + "2017-03-12", + 11 + ], + [ + "2017-03-13", + 97 + ], + [ + "2017-03-14", + 77 + ], + [ + "2017-03-15", + 77 + ], + [ + "2017-03-16", + 11 + ], + [ + "2017-03-17", + 164 + ], + [ + "2017-03-18", + 6 + ], + [ + "2017-03-19", + 45 + ], + [ + "2017-03-20", + 87 + ], + [ + "2017-03-21", + 71 + ], + [ + "2017-03-22", + 188 + ], + [ + "2017-03-23", + 166 + ], + [ + "2017-03-24", + 18 + ], + [ + "2017-03-25", + 90 + ], + [ + "2017-03-26", + 17 + ], + [ + "2017-03-27", + 99 + ], + [ + "2017-03-28", + 184 + ], + [ + "2017-03-29", + 56 + ], + [ + "2017-03-30", + 109 + ], + [ + "2017-03-31", + 692 + ], + [ + "2017-04-01", + 358 + ], + [ + "2017-04-02", + 87 + ], + [ + "2017-04-03", + 154 + ], + [ + "2017-04-04", + 174 + ], + [ + "2017-04-05", + 90 + ], + [ + "2017-04-06", + 46 + ], + [ + "2017-04-07", + 113 + ], + [ + "2017-04-08", + 67 + ], + [ + "2017-04-09", + 34 + ], + [ + "2017-04-10", + 105 + ], + [ + "2017-04-11", + 94 + ], + [ + "2017-04-12", + 205 + ], + [ + "2017-04-13", + 145 + ], + [ + "2017-04-14", + 90 + ], + [ + "2017-04-15", + 116 + ], + [ + "2017-04-16", + 46 + ], + [ + "2017-04-17", + 111 + ], + [ + "2017-04-18", + 182 + ], + [ + "2017-04-19", + 425 + ], + [ + "2017-04-20", + 128 + ], + [ + "2017-04-21", + 213 + ], + [ + "2017-04-22", + 60 + ], + [ + "2017-04-23", + 59 + ], + [ + "2017-04-24", + 74 + ], + [ + "2017-04-25", + 121 + ], + [ + "2017-04-26", + 393 + ], + [ + "2017-04-27", + 102 + ], + [ + "2017-04-28", + 30 + ], + [ + "2017-04-29", + 46 + ], + [ + "2017-04-30", + 109 + ], + [ + "2017-05-01", + 131 + ], + [ + "2017-05-02", + 74 + ], + [ + "2017-05-03", + 113 + ], + [ + "2017-05-04", + 91 + ], + [ + "2017-05-05", + 20 + ], + [ + "2017-05-06", + 58 + ], + [ + "2017-05-07", + 25 + ], + [ + "2017-05-08", + 66 + ], + [ + "2017-05-09", + 100 + ], + [ + "2017-05-10", + 222 + ], + [ + "2017-05-11", + 82 + ], + [ + "2017-05-12", + 121 + ], + [ + "2017-05-13", + 36 + ], + [ + "2017-05-14", + 33 + ], + [ + "2017-05-15", + 70 + ], + [ + "2017-05-16", + 76 + ], + [ + "2017-05-17", + 49 + ], + [ + "2017-05-18", + 105 + ], + [ + "2017-05-19", + 209 + ], + [ + "2017-05-20", + 23 + ], + [ + "2017-05-21", + 28 + ], + [ + "2017-05-22", + 24 + ], + [ + "2017-05-23", + 78 + ], + [ + "2017-05-24", + 41 + ], + [ + "2017-05-25", + 169 + ], + [ + "2017-05-26", + 28 + ], + [ + "2017-05-27", + 79 + ], + [ + "2017-05-28", + 19 + ], + [ + "2017-05-29", + 97 + ], + [ + "2017-05-30", + 257 + ], + [ + "2017-05-31", + 54 + ], + [ + "2017-06-01", + 109 + ], + [ + "2017-06-02", + 12 + ], + [ + "2017-06-03", + 18 + ], + [ + "2017-06-04", + 11 + ], + [ + "2017-06-05", + 81 + ], + [ + "2017-06-06", + 182 + ], + [ + "2017-06-07", + 149 + ], + [ + "2017-06-08", + 82 + ], + [ + "2017-06-09", + 6 + ], + [ + "2017-06-10", + 64 + ], + [ + "2017-06-11", + 30 + ], + [ + "2017-06-12", + 50 + ], + [ + "2017-06-13", + 576 + ], + [ + "2017-06-14", + 171 + ], + [ + "2017-06-15", + 156 + ], + [ + "2017-06-16", + 133 + ], + [ + "2017-06-17", + 121 + ], + [ + "2017-06-18", + 36 + ], + [ + "2017-06-19", + 83 + ], + [ + "2017-06-20", + 141 + ], + [ + "2017-06-21", + 119 + ], + [ + "2017-06-22", + 148 + ], + [ + "2017-06-23", + 83 + ], + [ + "2017-06-24", + 157 + ], + [ + "2017-06-25", + 32 + ], + [ + "2017-06-26", + 236 + ], + [ + "2017-06-27", + 109 + ], + [ + "2017-06-28", + 147 + ], + [ + "2017-06-29", + 117 + ], + [ + "2017-06-30", + 95 + ], + [ + "2017-07-01", + 23 + ], + [ + "2017-07-02", + 16 + ], + [ + "2017-07-03", + 40 + ], + [ + "2017-07-04", + 55 + ], + [ + "2017-07-05", + 164 + ], + [ + "2017-07-06", + 44 + ], + [ + "2017-07-07", + 172 + ], + [ + "2017-07-08", + 35 + ], + [ + "2017-07-09", + 25 + ], + [ + "2017-07-10", + 78 + ], + [ + "2017-07-11", + 118 + ], + [ + "2017-07-12", + 149 + ], + [ + "2017-07-13", + 31 + ], + [ + "2017-07-14", + 29 + ], + [ + "2017-07-15", + 45 + ], + [ + "2017-07-16", + 74 + ], + [ + "2017-07-17", + 50 + ], + [ + "2017-07-18", + 114 + ], + [ + "2017-07-19", + 206 + ], + [ + "2017-07-20", + 82 + ], + [ + "2017-07-21", + 148 + ], + [ + "2017-07-22", + 67 + ], + [ + "2017-07-23", + 17 + ], + [ + "2017-07-24", + 158 + ], + [ + "2017-07-25", + 52 + ], + [ + "2017-07-26", + 96 + ], + [ + "2017-07-27", + 84 + ], + [ + "2017-07-28", + 32 + ], + [ + "2017-07-29", + 70 + ], + [ + "2017-07-30", + 30 + ], + [ + "2017-07-31", + 55 + ], + [ + "2017-08-01", + 67 + ], + [ + "2017-08-02", + 78 + ], + [ + "2017-08-03", + 56 + ], + [ + "2017-08-04", + 208 + ], + [ + "2017-08-05", + 37 + ], + [ + "2017-08-06", + 22 + ], + [ + "2017-08-07", + 86 + ], + [ + "2017-08-08", + 104 + ], + [ + "2017-08-09", + 49 + ], + [ + "2017-08-10", + 50 + ], + [ + "2017-08-11", + 57 + ], + [ + "2017-08-12", + 77 + ], + [ + "2017-08-13", + 52 + ], + [ + "2017-08-14", + 77 + ], + [ + "2017-08-15", + 129 + ], + [ + "2017-08-16", + 90 + ], + [ + "2017-08-17", + 164 + ], + [ + "2017-08-18", + 26 + ], + [ + "2017-08-19", + 31 + ], + [ + "2017-08-20", + 29 + ], + [ + "2017-08-21", + 85 + ], + [ + "2017-08-22", + 179 + ], + [ + "2017-08-23", + 55 + ], + [ + "2017-08-24", + 54 + ], + [ + "2017-08-25", + 22 + ], + [ + "2017-08-26", + 138 + ], + [ + "2017-08-27", + 45 + ], + [ + "2017-08-28", + 127 + ], + [ + "2017-08-29", + 67 + ], + [ + "2017-08-30", + 115 + ], + [ + "2017-08-31", + 60 + ], + [ + "2017-09-01", + 142 + ], + [ + "2017-09-02", + 64 + ], + [ + "2017-09-03", + 191 + ], + [ + "2017-09-04", + 64 + ], + [ + "2017-09-05", + 58 + ], + [ + "2017-09-06", + 75 + ], + [ + "2017-09-07", + 26 + ], + [ + "2017-09-08", + 42 + ], + [ + "2017-09-09", + 27 + ], + [ + "2017-09-10", + 12 + ], + [ + "2017-09-11", + 32 + ], + [ + "2017-09-12", + 72 + ], + [ + "2017-09-13", + 158 + ], + [ + "2017-09-14", + 45 + ], + [ + "2017-09-15", + 36 + ], + [ + "2017-09-16", + 8 + ], + [ + "2017-09-17", + 69 + ], + [ + "2017-09-18", + 161 + ], + [ + "2017-09-19", + 19 + ], + [ + "2017-09-20", + 97 + ], + [ + "2017-09-21", + 91 + ], + [ + "2017-09-22", + 20 + ], + [ + "2017-09-23", + 47 + ], + [ + "2017-09-24", + 32 + ], + [ + "2017-09-25", + 26 + ], + [ + "2017-09-26", + 27 + ], + [ + "2017-09-27", + 258 + ], + [ + "2017-09-28", + 96 + ], + [ + "2017-09-29", + 108 + ], + [ + "2017-09-30", + 17 + ], + [ + "2017-10-01", + 35 + ], + [ + "2017-10-02", + 43 + ], + [ + "2017-10-03", + 72 + ], + [ + "2017-10-04", + 22 + ], + [ + "2017-10-05", + 27 + ], + [ + "2017-10-06", + 84 + ], + [ + "2017-10-07", + 59 + ], + [ + "2017-10-08", + 75 + ], + [ + "2017-10-09", + 66 + ], + [ + "2017-10-10", + 37 + ], + [ + "2017-10-11", + 45 + ], + [ + "2017-10-12", + 184 + ], + [ + "2017-10-13", + 185 + ], + [ + "2017-10-14", + 125 + ], + [ + "2017-10-15", + 121 + ], + [ + "2017-10-16", + 116 + ], + [ + "2017-10-17", + 183 + ], + [ + "2017-10-18", + 154 + ], + [ + "2017-10-19", + 284 + ], + [ + "2017-10-20", + 98 + ], + [ + "2017-10-21", + 41 + ], + [ + "2017-10-22", + 95 + ], + [ + "2017-10-23", + 237 + ], + [ + "2017-10-24", + 127 + ], + [ + "2017-10-25", + 160 + ], + [ + "2017-10-26", + 74 + ], + [ + "2017-10-27", + 101 + ], + [ + "2017-10-28", + 60 + ], + [ + "2017-10-29", + 35 + ], + [ + "2017-10-30", + 202 + ], + [ + "2017-10-31", + 218 + ], + [ + "2017-11-01", + 187 + ], + [ + "2017-11-02", + 93 + ], + [ + "2017-11-03", + 190 + ], + [ + "2017-11-04", + 23 + ], + [ + "2017-11-05", + 118 + ], + [ + "2017-11-06", + 347 + ], + [ + "2017-11-07", + 139 + ], + [ + "2017-11-08", + 244 + ], + [ + "2017-11-09", + 199 + ], + [ + "2017-11-10", + 145 + ], + [ + "2017-11-11", + 70 + ], + [ + "2017-11-12", + 152 + ], + [ + "2017-11-13", + 120 + ], + [ + "2017-11-14", + 185 + ], + [ + "2017-11-15", + 61 + ], + [ + "2017-11-16", + 98 + ], + [ + "2017-11-17", + 104 + ], + [ + "2017-11-18", + 2 + ], + [ + "2017-11-19", + 65 + ], + [ + "2017-11-20", + 194 + ], + [ + "2017-11-21", + 153 + ], + [ + "2017-11-22", + 102 + ], + [ + "2017-11-23", + 56 + ], + [ + "2017-11-24", + 107 + ], + [ + "2017-11-25", + 62 + ], + [ + "2017-11-26", + 40 + ], + [ + "2017-11-27", + 53 + ], + [ + "2017-11-28", + 83 + ], + [ + "2017-11-29", + 150 + ], + [ + "2017-11-30", + 147 + ], + [ + "2017-12-01", + 169 + ], + [ + "2017-12-02", + 44 + ], + [ + "2017-12-03", + 48 + ], + [ + "2017-12-04", + 118 + ], + [ + "2017-12-05", + 184 + ], + [ + "2017-12-06", + 297 + ], + [ + "2017-12-07", + 244 + ], + [ + "2017-12-08", + 213 + ], + [ + "2017-12-09", + 37 + ], + [ + "2017-12-10", + 30 + ], + [ + "2017-12-11", + 209 + ], + [ + "2017-12-12", + 102 + ], + [ + "2017-12-13", + 92 + ], + [ + "2017-12-14", + 140 + ], + [ + "2017-12-15", + 57 + ], + [ + "2017-12-16", + 2 + ], + [ + "2017-12-17", + 53 + ], + [ + "2017-12-18", + 76 + ], + [ + "2017-12-19", + 37 + ], + [ + "2017-12-20", + 60 + ], + [ + "2017-12-21", + 82 + ], + [ + "2017-12-22", + 127 + ], + [ + "2017-12-23", + 97 + ], + [ + "2017-12-24", + 68 + ], + [ + "2017-12-25", + 4 + ], + [ + "2017-12-26", + 38 + ], + [ + "2017-12-27", + 73 + ], + [ + "2017-12-28", + 130 + ], + [ + "2017-12-29", + 120 + ], + [ + "2017-12-30", + 59 + ], + [ + "2017-12-31", + 141 + ], + [ + "2018-01-01", + 168 + ], + [ + "2018-01-02", + 170 + ], + [ + "2018-01-03", + 219 + ], + [ + "2018-01-04", + 55 + ], + [ + "2018-01-05", + 112 + ], + [ + "2018-01-06", + 53 + ], + [ + "2018-01-07", + 49 + ], + [ + "2018-01-08", + 82 + ], + [ + "2018-01-09", + 173 + ], + [ + "2018-01-10", + 107 + ], + [ + "2018-01-11", + 92 + ], + [ + "2018-01-12", + 211 + ], + [ + "2018-01-13", + 160 + ], + [ + "2018-01-14", + 14 + ], + [ + "2018-01-15", + 73 + ], + [ + "2018-01-16", + 179 + ], + [ + "2018-01-17", + 106 + ], + [ + "2018-01-18", + 90 + ], + [ + "2018-01-19", + 74 + ], + [ + "2018-01-20", + 149 + ], + [ + "2018-01-21", + 109 + ], + [ + "2018-01-22", + 82 + ], + [ + "2018-01-23", + 206 + ], + [ + "2018-01-24", + 65 + ], + [ + "2018-01-25", + 122 + ], + [ + "2018-01-26", + 131 + ], + [ + "2018-01-27", + 34 + ], + [ + "2018-01-28", + 53 + ], + [ + "2018-01-29", + 100 + ], + [ + "2018-01-30", + 104 + ], + [ + "2018-01-31", + 149 + ], + [ + "2018-02-01", + 56 + ], + [ + "2018-02-02", + 5 + ], + [ + "2018-02-03", + 31 + ], + [ + "2018-02-04", + 72 + ], + [ + "2018-02-05", + 79 + ], + [ + "2018-02-06", + 56 + ], + [ + "2018-02-07", + 158 + ], + [ + "2018-02-08", + 207 + ], + [ + "2018-02-09", + 128 + ], + [ + "2018-02-10", + 34 + ], + [ + "2018-02-11", + 105 + ], + [ + "2018-02-12", + 91 + ], + [ + "2018-02-13", + 96 + ], + [ + "2018-02-14", + 133 + ], + [ + "2018-02-15", + 203 + ], + [ + "2018-02-16", + 49 + ], + [ + "2018-02-17", + 42 + ], + [ + "2018-02-18", + 34 + ], + [ + "2018-02-19", + 122 + ], + [ + "2018-02-20", + 141 + ], + [ + "2018-02-21", + 111 + ], + [ + "2018-02-22", + 144 + ], + [ + "2018-02-23", + 132 + ], + [ + "2018-02-24", + 53 + ], + [ + "2018-02-25", + 44 + ], + [ + "2018-02-26", + 142 + ], + [ + "2018-02-27", + 154 + ], + [ + "2018-02-28", + 75 + ], + [ + "2018-03-01", + 194 + ], + [ + "2018-03-02", + 160 + ], + [ + "2018-03-03", + 33 + ], + [ + "2018-03-04", + 43 + ], + [ + "2018-03-05", + 150 + ], + [ + "2018-03-06", + 165 + ], + [ + "2018-03-07", + 187 + ], + [ + "2018-03-08", + 132 + ], + [ + "2018-03-09", + 38 + ], + [ + "2018-03-10", + 100 + ], + [ + "2018-03-11", + 73 + ], + [ + "2018-03-12", + 644 + ], + [ + "2018-03-13", + 945 + ], + [ + "2018-03-14", + 734 + ], + [ + "2018-03-15", + 315 + ], + [ + "2018-03-16", + 184 + ], + [ + "2018-03-17", + 70 + ], + [ + "2018-03-18", + 83 + ], + [ + "2018-03-19", + 1699 + ], + [ + "2018-03-20", + 1241 + ], + [ + "2018-03-21", + 265 + ], + [ + "2018-03-22", + 179 + ], + [ + "2018-03-23", + 114 + ], + [ + "2018-03-24", + 24 + ], + [ + "2018-03-25", + 56 + ], + [ + "2018-03-26", + 177 + ], + [ + "2018-03-27", + 332 + ], + [ + "2018-03-28", + 213 + ], + [ + "2018-03-29", + 58 + ], + [ + "2018-03-30", + 157 + ], + [ + "2018-03-31", + 35 + ], + [ + "2018-04-01", + 27 + ], + [ + "2018-04-02", + 30 + ], + [ + "2018-04-03", + 113 + ], + [ + "2018-04-04", + 65 + ], + [ + "2018-04-05", + 168 + ], + [ + "2018-04-06", + 250 + ], + [ + "2018-04-07", + 64 + ], + [ + "2018-04-08", + 76 + ], + [ + "2018-04-09", + 101 + ], + [ + "2018-04-10", + 218 + ], + [ + "2018-04-11", + 233 + ], + [ + "2018-04-12", + 116 + ], + [ + "2018-04-13", + 74 + ], + [ + "2018-04-14", + 73 + ], + [ + "2018-04-15", + 18 + ], + [ + "2018-04-16", + 36 + ], + [ + "2018-04-17", + 140 + ], + [ + "2018-04-18", + 54 + ], + [ + "2018-04-19", + 77 + ], + [ + "2018-04-20", + 103 + ], + [ + "2018-04-21", + 48 + ], + [ + "2018-04-22", + 36 + ], + [ + "2018-04-23", + 83 + ], + [ + "2018-04-24", + 149 + ], + [ + "2018-04-25", + 86 + ], + [ + "2018-04-26", + 34 + ], + [ + "2018-04-27", + 124 + ], + [ + "2018-04-28", + 15 + ], + [ + "2018-04-29", + 64 + ], + [ + "2018-04-30", + 27 + ], + [ + "2018-05-01", + 223 + ], + [ + "2018-05-02", + 276 + ], + [ + "2018-05-03", + 205 + ], + [ + "2018-05-04", + 112 + ], + [ + "2018-05-05", + 67 + ], + [ + "2018-05-06", + 115 + ], + [ + "2018-05-07", + 138 + ], + [ + "2018-05-08", + 89 + ], + [ + "2018-05-09", + 121 + ], + [ + "2018-05-10", + 41 + ], + [ + "2018-05-11", + 30 + ], + [ + "2018-05-12", + 59 + ], + [ + "2018-05-13", + 47 + ], + [ + "2018-05-14", + 145 + ], + [ + "2018-05-15", + 19 + ], + [ + "2018-05-16", + 171 + ], + [ + "2018-05-17", + 143 + ], + [ + "2018-05-18", + 62 + ], + [ + "2018-05-19", + 110 + ], + [ + "2018-05-20", + 64 + ], + [ + "2018-05-21", + 75 + ], + [ + "2018-05-22", + 89 + ], + [ + "2018-05-23", + 82 + ], + [ + "2018-05-24", + 84 + ], + [ + "2018-05-25", + 100 + ], + [ + "2018-05-26", + 131 + ], + [ + "2018-05-27", + 70 + ], + [ + "2018-05-28", + 268 + ], + [ + "2018-05-29", + 85 + ], + [ + "2018-05-30", + 158 + ], + [ + "2018-05-31", + 117 + ], + [ + "2018-06-01", + 137 + ], + [ + "2018-06-02", + 48 + ], + [ + "2018-06-03", + 76 + ], + [ + "2018-06-04", + 197 + ], + [ + "2018-06-05", + 151 + ], + [ + "2018-06-06", + 124 + ], + [ + "2018-06-07", + 102 + ], + [ + "2018-06-08", + 141 + ], + [ + "2018-06-09", + 97 + ], + [ + "2018-06-10", + 80 + ], + [ + "2018-06-11", + 190 + ], + [ + "2018-06-12", + 75 + ], + [ + "2018-06-13", + 101 + ], + [ + "2018-06-14", + 124 + ], + [ + "2018-06-15", + 188 + ], + [ + "2018-06-16", + 67 + ], + [ + "2018-06-17", + 54 + ], + [ + "2018-06-18", + 101 + ], + [ + "2018-06-19", + 145 + ], + [ + "2018-06-20", + 84 + ], + [ + "2018-06-21", + 114 + ], + [ + "2018-06-22", + 82 + ], + [ + "2018-06-23", + 92 + ], + [ + "2018-06-24", + 36 + ], + [ + "2018-06-25", + 170 + ], + [ + "2018-06-26", + 132 + ], + [ + "2018-06-27", + 157 + ], + [ + "2018-06-28", + 171 + ], + [ + "2018-06-29", + 83 + ], + [ + "2018-06-30", + 42 + ], + [ + "2018-07-01", + 59 + ], + [ + "2018-07-02", + 224 + ], + [ + "2018-07-03", + 208 + ], + [ + "2018-07-04", + 68 + ], + [ + "2018-07-05", + 128 + ], + [ + "2018-07-06", + 62 + ], + [ + "2018-07-07", + 74 + ], + [ + "2018-07-08", + 19 + ], + [ + "2018-07-09", + 68 + ], + [ + "2018-07-10", + 152 + ], + [ + "2018-07-11", + 159 + ], + [ + "2018-07-12", + 45 + ], + [ + "2018-07-13", + 24 + ], + [ + "2018-07-14", + 30 + ], + [ + "2018-07-15", + 135 + ], + [ + "2018-07-16", + 132 + ], + [ + "2018-07-17", + 388 + ], + [ + "2018-07-18", + 293 + ], + [ + "2018-07-19", + 106 + ], + [ + "2018-07-20", + 70 + ], + [ + "2018-07-21", + 23 + ], + [ + "2018-07-22", + 23 + ], + [ + "2018-07-23", + 112 + ], + [ + "2018-07-24", + 151 + ], + [ + "2018-07-25", + 170 + ], + [ + "2018-07-26", + 108 + ], + [ + "2018-07-27", + 18 + ], + [ + "2018-07-28", + 118 + ], + [ + "2018-07-29", + 52 + ], + [ + "2018-07-30", + 90 + ], + [ + "2018-07-31", + 228 + ], + [ + "2018-08-01", + 98 + ], + [ + "2018-08-02", + 111 + ], + [ + "2018-08-03", + 81 + ], + [ + "2018-08-04", + 55 + ], + [ + "2018-08-05", + 94 + ], + [ + "2018-08-06", + 116 + ], + [ + "2018-08-07", + 96 + ], + [ + "2018-08-08", + 103 + ], + [ + "2018-08-09", + 81 + ], + [ + "2018-08-10", + 112 + ], + [ + "2018-08-11", + 20 + ], + [ + "2018-08-12", + 48 + ], + [ + "2018-08-13", + 120 + ], + [ + "2018-08-14", + 67 + ], + [ + "2018-08-15", + 108 + ], + [ + "2018-08-16", + 125 + ], + [ + "2018-08-17", + 45 + ], + [ + "2018-08-18", + 12 + ], + [ + "2018-08-19", + 66 + ], + [ + "2018-08-20", + 110 + ], + [ + "2018-08-21", + 286 + ], + [ + "2018-08-22", + 217 + ], + [ + "2018-08-23", + 169 + ], + [ + "2018-08-24", + 83 + ], + [ + "2018-08-25", + 31 + ], + [ + "2018-08-26", + 114 + ], + [ + "2018-08-27", + 150 + ], + [ + "2018-08-28", + 108 + ], + [ + "2018-08-29", + 79 + ], + [ + "2018-08-30", + 42 + ], + [ + "2018-08-31", + 150 + ], + [ + "2018-09-01", + 56 + ], + [ + "2018-09-02", + 59 + ], + [ + "2018-09-03", + 87 + ], + [ + "2018-09-04", + 132 + ], + [ + "2018-09-05", + 79 + ], + [ + "2018-09-06", + 139 + ], + [ + "2018-09-07", + 104 + ], + [ + "2018-09-08", + 21 + ], + [ + "2018-09-09", + 46 + ], + [ + "2018-09-10", + 18 + ], + [ + "2018-09-11", + 146 + ], + [ + "2018-09-12", + 109 + ], + [ + "2018-09-13", + 50 + ], + [ + "2018-09-14", + 65 + ], + [ + "2018-09-15", + 47 + ], + [ + "2018-09-16", + 56 + ], + [ + "2018-09-17", + 117 + ], + [ + "2018-09-18", + 353 + ], + [ + "2018-09-19", + 99 + ], + [ + "2018-09-20", + 142 + ], + [ + "2018-09-21", + 221 + ], + [ + "2018-09-22", + 110 + ], + [ + "2018-09-23", + 49 + ], + [ + "2018-09-24", + 58 + ], + [ + "2018-09-25", + 142 + ], + [ + "2018-09-26", + 142 + ], + [ + "2018-09-27", + 69 + ], + [ + "2018-09-28", + 28 + ], + [ + "2018-09-29", + 52 + ], + [ + "2018-09-30", + 55 + ], + [ + "2018-10-01", + 56 + ], + [ + "2018-10-02", + 14 + ], + [ + "2018-10-03", + 81 + ], + [ + "2018-10-04", + 40 + ], + [ + "2018-10-05", + 49 + ], + [ + "2018-10-06", + 154 + ], + [ + "2018-10-07", + 86 + ], + [ + "2018-10-08", + 131 + ], + [ + "2018-10-09", + 160 + ], + [ + "2018-10-10", + 96 + ], + [ + "2018-10-11", + 114 + ], + [ + "2018-10-12", + 115 + ], + [ + "2018-10-13", + 125 + ], + [ + "2018-10-14", + 170 + ], + [ + "2018-10-15", + 131 + ], + [ + "2018-10-16", + 118 + ], + [ + "2018-10-17", + 47 + ], + [ + "2018-10-18", + 153 + ], + [ + "2018-10-19", + 212 + ], + [ + "2018-10-20", + 191 + ], + [ + "2018-10-21", + 105 + ], + [ + "2018-10-22", + 157 + ], + [ + "2018-10-23", + 386 + ], + [ + "2018-10-24", + 179 + ], + [ + "2018-10-25", + 285 + ], + [ + "2018-10-26", + 151 + ], + [ + "2018-10-27", + 121 + ], + [ + "2018-10-28", + 104 + ], + [ + "2018-10-29", + 86 + ], + [ + "2018-10-30", + 225 + ], + [ + "2018-10-31", + 102 + ], + [ + "2018-11-01", + 106 + ], + [ + "2018-11-02", + 219 + ], + [ + "2018-11-03", + 122 + ], + [ + "2018-11-04", + 85 + ], + [ + "2018-11-05", + 578 + ], + [ + "2018-11-06", + 429 + ], + [ + "2018-11-07", + 282 + ], + [ + "2018-11-08", + 245 + ], + [ + "2018-11-09", + 197 + ], + [ + "2018-11-10", + 182 + ], + [ + "2018-11-11", + 80 + ], + [ + "2018-11-12", + 207 + ], + [ + "2018-11-13", + 187 + ], + [ + "2018-11-14", + 75 + ], + [ + "2018-11-15", + 135 + ], + [ + "2018-11-16", + 152 + ], + [ + "2018-11-17", + 76 + ], + [ + "2018-11-18", + 83 + ], + [ + "2018-11-19", + 107 + ], + [ + "2018-11-20", + 73 + ], + [ + "2018-11-21", + 87 + ], + [ + "2018-11-22", + 69 + ], + [ + "2018-11-23", + 60 + ], + [ + "2018-11-24", + 57 + ], + [ + "2018-11-25", + 83 + ], + [ + "2018-11-26", + 118 + ], + [ + "2018-11-27", + 230 + ], + [ + "2018-11-28", + 98 + ], + [ + "2018-11-29", + 43 + ], + [ + "2018-11-30", + 114 + ], + [ + "2018-12-01", + 51 + ], + [ + "2018-12-02", + 110 + ], + [ + "2018-12-03", + 65 + ], + [ + "2018-12-04", + 53 + ], + [ + "2018-12-05", + 44 + ], + [ + "2018-12-06", + 197 + ], + [ + "2018-12-07", + 95 + ], + [ + "2018-12-08", + 13 + ], + [ + "2018-12-09", + 17 + ], + [ + "2018-12-10", + 59 + ], + [ + "2018-12-11", + 165 + ], + [ + "2018-12-12", + 115 + ], + [ + "2018-12-13", + 150 + ], + [ + "2018-12-14", + 83 + ], + [ + "2018-12-15", + 12 + ], + [ + "2018-12-16", + 156 + ], + [ + "2018-12-17", + 37 + ], + [ + "2018-12-18", + 113 + ], + [ + "2018-12-19", + 36 + ], + [ + "2018-12-20", + 61 + ], + [ + "2018-12-21", + 46 + ], + [ + "2018-12-22", + 50 + ], + [ + "2018-12-23", + 138 + ], + [ + "2018-12-24", + 137 + ], + [ + "2018-12-25", + 124 + ], + [ + "2018-12-26", + 119 + ], + [ + "2018-12-27", + 73 + ], + [ + "2018-12-28", + 102 + ], + [ + "2018-12-29", + 88 + ], + [ + "2018-12-30", + 64 + ], + [ + "2018-12-31", + 101 + ], + [ + "2019-01-01", + 52 + ], + [ + "2019-01-02", + 112 + ], + [ + "2019-01-03", + 191 + ], + [ + "2019-01-04", + 110 + ], + [ + "2019-01-05", + 8 + ], + [ + "2019-01-06", + 15 + ], + [ + "2019-01-07", + 107 + ], + [ + "2019-01-08", + 274 + ], + [ + "2019-01-09", + 156 + ], + [ + "2019-01-10", + 62 + ], + [ + "2019-01-11", + 86 + ], + [ + "2019-01-12", + 92 + ], + [ + "2019-01-13", + 56 + ], + [ + "2019-01-14", + 174 + ], + [ + "2019-01-15", + 167 + ], + [ + "2019-01-16", + 48 + ], + [ + "2019-01-17", + 177 + ], + [ + "2019-01-18", + 43 + ], + [ + "2019-01-19", + 19 + ], + [ + "2019-01-20", + 60 + ], + [ + "2019-01-21", + 111 + ], + [ + "2019-01-22", + 35 + ], + [ + "2019-01-23", + 114 + ], + [ + "2019-01-24", + 143 + ], + [ + "2019-01-25", + 199 + ], + [ + "2019-01-26", + 113 + ], + [ + "2019-01-27", + 46 + ], + [ + "2019-01-28", + 276 + ], + [ + "2019-01-29", + 74 + ], + [ + "2019-01-30", + 45 + ], + [ + "2019-01-31", + 334 + ], + [ + "2019-02-01", + 193 + ], + [ + "2019-02-02", + 25 + ], + [ + "2019-02-03", + 62 + ], + [ + "2019-02-04", + 78 + ], + [ + "2019-02-05", + 63 + ], + [ + "2019-02-06", + 71 + ], + [ + "2019-02-07", + 61 + ], + [ + "2019-02-08", + 112 + ], + [ + "2019-02-09", + 103 + ], + [ + "2019-02-10", + 30 + ], + [ + "2019-02-11", + 88 + ], + [ + "2019-02-12", + 149 + ], + [ + "2019-02-13", + 57 + ], + [ + "2019-02-14", + 143 + ], + [ + "2019-02-15", + 271 + ], + [ + "2019-02-16", + 152 + ], + [ + "2019-02-17", + 53 + ], + [ + "2019-02-18", + 157 + ], + [ + "2019-02-19", + 263 + ], + [ + "2019-02-20", + 123 + ], + [ + "2019-02-21", + 324 + ] + ], + "highest_month": 8800, + "highest_day_average": 283, + "highest_week_average": 334, + "post": null +} diff --git a/WordPressKitTests/StatsRemoteV2Tests.swift b/WordPressKitTests/StatsRemoteV2Tests.swift index eaaff073..9c31f98a 100644 --- a/WordPressKitTests/StatsRemoteV2Tests.swift +++ b/WordPressKitTests/StatsRemoteV2Tests.swift @@ -17,6 +17,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { let getReferrersMockFilename = "stats-referrer-data.json" let getPostsMockFilename = "stats-posts-data.json" let getPublishedPostsFilename = "stats-published-posts.json" + let getPostsDetailsFilename = "stats-post-details.json" // MARK: - Properties @@ -29,6 +30,7 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { var siteReferrerDataEndpoint: String { return "sites/\(siteID)/stats/referrers/" } var sitePostsDataEndpoint: String { return "sites/\(siteID)/stats/top-posts/" } var sitePublishedPostsEndpoint: String { return "sites/\(siteID)/posts/" } + var sitePostDetailsEndpoint: String { return "sites/\(siteID)/post/9001" } var remote: StatsServiceRemoteV2! @@ -358,5 +360,79 @@ class StatsRemoteV2Tests: RemoteTestCase, RESTTestable { waitForExpectations(timeout: timeout, handler: nil) - } + } + + func testFetchPostDetail() { + let expect = expectation(description: "It should return post detail") + + stubRemoteResponse(sitePostDetailsEndpoint, filename: getPostsDetailsFilename, contentType: .ApplicationJSON) + + let feb21 = DateComponents(year: 2019, month: 2, day: 21) + let date = Calendar.autoupdatingCurrent.date(from: feb21)! + + remote.getDetails(forPostID: 9001) { (postDetails, error) in + XCTAssertNil(error) + XCTAssertNotNil(postDetails) + + XCTAssertEqual(postDetails?.fetchedDate, date) + XCTAssertEqual(postDetails?.totalViewsCount, 163343) + + XCTAssertEqual(postDetails?.dailyAveragesPerMonth.count, postDetails?.monthlyBreakdown.count) + XCTAssertEqual(postDetails?.dailyAveragesPerMonth.count, 10 + 12 + 12 + 12 + 2) + + let feb19Averages = postDetails?.dailyAveragesPerMonth.first { $0.date == DateComponents(year: 2019, month: 2) } + XCTAssertNotNil(feb19Averages) + XCTAssertEqual(feb19Averages?.period, .month) + XCTAssertEqual(feb19Averages?.viewsCount, 112) + + let feb19Views = postDetails?.monthlyBreakdown.first { $0.date == DateComponents(year: 2019, month: 2) } + XCTAssertNotNil(feb19Views) + XCTAssertEqual(feb19Views?.period, .month) + XCTAssertEqual(feb19Views?.viewsCount, 2578) + + XCTAssertEqual(postDetails?.lastTwoWeeks.count, 14) + + XCTAssertEqual(postDetails?.lastTwoWeeks.first?.viewsCount, 112) + XCTAssertEqual(postDetails?.lastTwoWeeks.first?.period, .day) + XCTAssertEqual(postDetails?.lastTwoWeeks.first?.date, DateComponents(year: 2019, month: 2, day: 08)) + + XCTAssertEqual(postDetails?.lastTwoWeeks.last?.viewsCount, 324) + XCTAssertEqual(postDetails?.lastTwoWeeks.last?.period, .day) + XCTAssertEqual(postDetails?.lastTwoWeeks.last?.date, DateComponents(year: 2019, month: 2, day: 21)) + + XCTAssertEqual(postDetails?.recentWeeks.count, 6) + + let leastRecentWeek = postDetails?.recentWeeks.first + let mostRecentWeek = postDetails?.recentWeeks.last + + XCTAssertNotNil(leastRecentWeek) + XCTAssertNotNil(mostRecentWeek) + + XCTAssertEqual(leastRecentWeek?.totalViewsCount, 688) + XCTAssertEqual(leastRecentWeek?.averageViewsCount, 98) + XCTAssertEqual(leastRecentWeek!.changePercentage, 0.0, accuracy: 0.0000000001) + XCTAssertEqual(leastRecentWeek?.startDay, DateComponents(year: 2019, month: 01, day: 14)) + XCTAssertEqual(leastRecentWeek?.endDay, DateComponents(year: 2019, month: 01, day: 20)) + XCTAssertEqual(leastRecentWeek?.days.count, 7) + XCTAssertEqual(leastRecentWeek?.days.first?.date, leastRecentWeek?.startDay) + XCTAssertEqual(leastRecentWeek?.days.last?.date, leastRecentWeek?.endDay) + XCTAssertEqual(leastRecentWeek?.days.first?.viewsCount, 174) + XCTAssertEqual(leastRecentWeek?.days.last?.viewsCount, 60) + + XCTAssertEqual(mostRecentWeek?.totalViewsCount, 867) + XCTAssertEqual(mostRecentWeek?.averageViewsCount, 181) + XCTAssertEqual(mostRecentWeek!.changePercentage, 38.7732, accuracy: 0.001) + XCTAssertEqual(mostRecentWeek?.startDay, DateComponents(year: 2019, month: 02, day: 18)) + XCTAssertEqual(mostRecentWeek?.endDay, DateComponents(year: 2019, month: 02, day: 21)) + XCTAssertEqual(mostRecentWeek?.days.count, 4) + XCTAssertEqual(mostRecentWeek?.days.first?.date, mostRecentWeek?.startDay) + XCTAssertEqual(mostRecentWeek?.days.last?.date, mostRecentWeek?.endDay) + XCTAssertEqual(mostRecentWeek?.days.first?.viewsCount, 157) + XCTAssertEqual(mostRecentWeek?.days.last?.viewsCount, 324) + + expect.fulfill() + } + + waitForExpectations(timeout: timeout, handler: nil) + } } From 1435aeb442f5f2e8d33524d18317c872bac9e71b Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Fri, 22 Feb 2019 21:35:04 +0100 Subject: [PATCH 25/27] fix typo in comment --- WordPressKit/Time-based data/SummaryStatsType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit/Time-based data/SummaryStatsType.swift b/WordPressKit/Time-based data/SummaryStatsType.swift index aefac767..8b433c4e 100644 --- a/WordPressKit/Time-based data/SummaryStatsType.swift +++ b/WordPressKit/Time-based data/SummaryStatsType.swift @@ -111,7 +111,7 @@ private extension StatsSummaryData { // We have our own handrolled date format for data broken up on week basis. // Example dates in this format are `2019W02W18` or `2019W02W11`. - // The structure is rougly `aaaaWbbWcc`, where: + // The structure is `aaaaWbbWcc`, where: // - `aaaa` is four-digit year number, // - `bb` is two-digit month number // - `cc` is two-digit day number From 0f98a2c3c5ed1a2087bba11892cd64d65ae44589 Mon Sep 17 00:00:00 2001 From: Jan Klausa Date: Fri, 22 Feb 2019 22:39:32 +0100 Subject: [PATCH 26/27] fix broken merge --- WordPressKit.xcodeproj/project.pbxproj | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 495de286..3ae86ecf 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -32,16 +32,17 @@ 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 */; }; - 40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819777221F00E600A298E4 /* SummaryStatsType.swift */; }; - 4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819779221F153A00A298E4 /* stats-visits-week.json */; }; - 4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977A221F153A00A298E4 /* stats-visits-day.json */; }; - 4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977D221F269A00A298E4 /* stats-visits-month.json */; }; 4081976F221DDE9B00A298E4 /* PostsStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */; }; 40819771221DFDB700A298E4 /* stats-posts-data.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819770221DFDB600A298E4 /* stats-posts-data.json */; }; 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 */; }; + 40819778221F00E600A298E4 /* SummaryStatsType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819777221F00E600A298E4 /* SummaryStatsType.swift */; }; + 4081977B221F153B00A298E4 /* stats-visits-week.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819779221F153A00A298E4 /* stats-visits-week.json */; }; + 4081977C221F153B00A298E4 /* stats-visits-day.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977A221F153A00A298E4 /* stats-visits-day.json */; }; + 4081977E221F269A00A298E4 /* stats-visits-month.json in Resources */ = {isa = PBXBuildFile; fileRef = 4081977D221F269A00A298E4 /* stats-visits-month.json */; }; 40819783221F5C8200A298E4 /* StatsPostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40819782221F5C8200A298E4 /* StatsPostDetails.swift */; }; 40819785221F74B200A298E4 /* stats-post-details.json in Resources */ = {isa = PBXBuildFile; fileRef = 40819784221F74B200A298E4 /* stats-post-details.json */; }; + 408197882220A35000A298E4 /* StatsLastPostInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408197872220A35000A298E4 /* StatsLastPostInsight.swift */; }; 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 */; }; @@ -475,7 +476,6 @@ E6C1E8491EF21FC100D139D9 /* is-passwordless-account-no-account-found.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8471EF21FC100D139D9 /* is-passwordless-account-no-account-found.json */; }; E6C1E84A1EF21FC100D139D9 /* is-passwordless-account-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C1E8481EF21FC100D139D9 /* is-passwordless-account-success.json */; }; E6D0EE621F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */; }; - F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */; }; FF20AD2220B8471A00082398 /* WordPressKit.podspec in Resources */ = {isa = PBXBuildFile; fileRef = FF20AD2120B8471A00082398 /* WordPressKit.podspec */; }; FFE247A720C891D1002DF3A2 /* WordPressComOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */; }; FFE247AF20C891E6002DF3A2 /* WordPressComOAuthWrongPasswordFail.json in Resources */ = {isa = PBXBuildFile; fileRef = FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */; }; @@ -527,16 +527,17 @@ 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 = ""; }; - 40819777221F00E600A298E4 /* SummaryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryStatsType.swift; sourceTree = ""; }; - 40819779221F153A00A298E4 /* stats-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-week.json"; sourceTree = ""; }; - 4081977A221F153A00A298E4 /* stats-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-day.json"; sourceTree = ""; }; - 4081977D221F269A00A298E4 /* stats-visits-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-month.json"; sourceTree = ""; }; 4081976E221DDE9B00A298E4 /* PostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsStatsType.swift; sourceTree = ""; }; 40819770221DFDB600A298E4 /* stats-posts-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-posts-data.json"; 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 = ""; }; + 40819777221F00E600A298E4 /* SummaryStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryStatsType.swift; sourceTree = ""; }; + 40819779221F153A00A298E4 /* stats-visits-week.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-week.json"; sourceTree = ""; }; + 4081977A221F153A00A298E4 /* stats-visits-day.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-day.json"; sourceTree = ""; }; + 4081977D221F269A00A298E4 /* stats-visits-month.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-visits-month.json"; sourceTree = ""; }; 40819782221F5C8200A298E4 /* StatsPostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPostDetails.swift; sourceTree = ""; }; 40819784221F74B200A298E4 /* stats-post-details.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-post-details.json"; sourceTree = ""; }; + 408197872220A35000A298E4 /* StatsLastPostInsight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatsLastPostInsight.swift; 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 = ""; }; @@ -984,7 +985,6 @@ E6D0EE611F7EF9CE0064D3FC /* AccountServiceRemoteREST+SocialService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountServiceRemoteREST+SocialService.swift"; sourceTree = ""; }; ED05C8FF3E61D93CE5BA527E /* Pods_WordPressKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; EFF80A6E6EE37118CB1DA158 /* Pods_WordPressKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsLastPostInsight.swift; sourceTree = ""; }; FF20AD2120B8471A00082398 /* WordPressKit.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = WordPressKit.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; FFE247A620C891D1002DF3A2 /* WordPressComOAuthTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressComOAuthTests.swift; sourceTree = ""; }; FFE247A820C891E5002DF3A2 /* WordPressComOAuthWrongPasswordFail.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = WordPressComOAuthWrongPasswordFail.json; sourceTree = ""; }; @@ -1052,7 +1052,7 @@ 40414061220F9F2800CF7C5B /* Insights */ = { isa = PBXGroup; children = ( - F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, + 408197872220A35000A298E4 /* StatsLastPostInsight.swift */, 4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */, 40E7FEA8220FA4050032834E /* StatsEmailFollowersInsight.swift */, 4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */, @@ -1071,7 +1071,6 @@ children = ( 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, - F96E0642221E15A9008E7D97 /* StatsLastPostInsight.swift */, 40819782221F5C8200A298E4 /* StatsPostDetails.swift */, ); name = V2; @@ -2294,6 +2293,7 @@ 7430C9B41F1927C50051B8E6 /* RemoteReaderSite.m in Sources */, 74585B8E1F0D51A100E7E667 /* DomainsServiceRemote.swift in Sources */, 7430C9A41F1927180051B8E6 /* ReaderPostServiceRemote.m in Sources */, + 408197882220A35000A298E4 /* StatsLastPostInsight.swift in Sources */, 74E2294E1F1E73FE0085F7F2 /* RemotePublicizeService.swift in Sources */, 9F3E0BA22087345F009CB5BA /* ServiceRequest.swift in Sources */, E1A6605F1FD694ED00BAC339 /* PluginDirectoryEntry.swift in Sources */, @@ -2352,7 +2352,6 @@ 93F50A381F226B9300B5BEBA /* WordPressComServiceRemote.m in Sources */, 9F4E52002088E38200424676 /* ObjectValidation.swift in Sources */, 7430C9B81F1927C50051B8E6 /* RemoteReaderTopic.m in Sources */, - F96E0643221E15A9008E7D97 /* StatsLastPostInsight.swift in Sources */, 7403A3021EF0726E00DED7DC /* AccountSettings.swift in Sources */, 40E7FEA9220FA4060032834E /* StatsEmailFollowersInsight.swift in Sources */, 404057DA221C9D560060250C /* ReferrerStatsType.swift in Sources */, From 818ac9dd0bd5674dbe1eb8c79cc67ca4a436c357 Mon Sep 17 00:00:00 2001 From: Lorenzo Mattei Date: Mon, 25 Feb 2019 16:42:45 +0100 Subject: [PATCH 27/27] Bump version to 2.1.0 --- WordPressKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit.podspec b/WordPressKit.podspec index d50b7c56..c7333816 100644 --- a/WordPressKit.podspec +++ b/WordPressKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressKit" - s.version = "2.1.0-beta.2" + s.version = "2.1.0" s.summary = "WordPressKit offers a clean and simple WordPress.com and WordPress.org API." s.description = <<-DESC