diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index b07961c3..49bdd1ab 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -18,6 +18,10 @@ 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 */; }; + 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 */; }; @@ -490,6 +494,10 @@ 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 = ""; }; + 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 = ""; }; @@ -987,6 +995,15 @@ name = Frameworks; sourceTree = ""; }; + 404057C3221B30140060250C /* Time-based data */ = { + isa = PBXGroup; + children = ( + 404057C4221B30400060250C /* SearchTermStatsType.swift */, + 404057C8221B789B0060250C /* AuthorsStatsType.swift */, + ); + path = "Time-based data"; + sourceTree = ""; + }; 40414061220F9F2800CF7C5B /* Insights */ = { isa = PBXGroup; children = ( @@ -1006,6 +1023,7 @@ 40B01BF3220E534900036D10 /* V2 */ = { isa = PBXGroup; children = ( + 404057C3221B30140060250C /* Time-based data */, 40414061220F9F2800CF7C5B /* Insights */, ); name = V2; @@ -1499,7 +1517,9 @@ 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 */, 826016F81F9FAF6300533B6C /* activity-log-bad-json-failure.json */, 826016F51F9FAF6300533B6C /* activity-log-success-1.json */, @@ -1965,6 +1985,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 */, @@ -2042,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 */, @@ -2281,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 */, @@ -2329,6 +2352,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 3d2c14a3..de5094be 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,44 @@ 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 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 + } + + 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 +158,48 @@ public protocol InsightProtocol { init?(jsonDictionary: [String: AnyObject]) } +// naming is hard. +public protocol TimeStatsProtocol { + static var pathComponent: String { get } + + var period: StatsPeriodUnit { get } + var periodEndDate: Date { get } + + 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. +fileprivate extension StatsPeriodUnit { + var stringValue: String { + switch self { + case .day: + return "day" + case .week: + return "week" + case .month: + return "month" + case .year: + 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 { // A big chunk of those use the same endpoint and queryProperties.. Let's simplify the protocol conformance in those cases. @@ -124,6 +213,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. 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/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 +} 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 + } + } +} 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) + + } }