Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions WordPressKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -517,6 +519,8 @@
404057DB221C9FD70060250C /* stats-referrer-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-referrer-data.json"; sourceTree = "<group>"; };
4041405D220F9EF500CF7C5B /* StatsDotComFollowersInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDotComFollowersInsight.swift; sourceTree = "<group>"; };
4041405F220F9F1F00CF7C5B /* StatsAllTimesInsight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsAllTimesInsight.swift; sourceTree = "<group>"; };
40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishedPostsStatsType.swift; sourceTree = "<group>"; };
40819774221E497C00A298E4 /* stats-published-posts.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "stats-published-posts.json"; sourceTree = "<group>"; };
40A71C6D220E1D8E002E3D25 /* StatsServiceRemoteV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsServiceRemoteV2.swift; sourceTree = "<group>"; };
40AB1AD9200FED25009B533D /* PluginDirectoryFeedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryFeedPage.swift; sourceTree = "<group>"; };
40E4698A2017C2840030DB5F /* plugin-directory-popular.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "plugin-directory-popular.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1022,6 +1026,7 @@
404057D1221C56AB0060250C /* CountryStatsType.swift */,
404057D5221C92660060250C /* ClicksStatsType.swift */,
404057D9221C9D560060250C /* ReferrerStatsType.swift */,
40819772221E10C900A298E4 /* PublishedPostsStatsType.swift */,
);
path = "Time-based data";
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
59 changes: 58 additions & 1 deletion WordPressKit/StatsServiceRemoteV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ 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)) {
Expand Down Expand Up @@ -159,6 +159,63 @@ 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 to 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)!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does jan1 mean "Jan is number 1!"? Subliminal messaging. I like it. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dang you caught me :D


return Calendar.autoupdatingCurrent.startOfDay(for: jan1)
}
}

}

Expand Down
39 changes: 39 additions & 0 deletions WordPressKit/Time-based data/PublishedPostsStatsType.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
20 changes: 20 additions & 0 deletions WordPressKitTests/Mock Data/stats-published-posts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"found": 3,
"posts": [
{
"ID": 41038,
"title": "Announcing Newspack by WordPress.com &#8212; 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 &#8216;Anything Is Possible&#8217; List",
"URL": "http://en.blog.wordpress.com/2019/01/03/introducing-the-2019-anything-is-possible-list/"
}
],
}
27 changes: 27 additions & 0 deletions WordPressKitTests/StatsRemoteV2Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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!

Expand Down Expand Up @@ -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 &#8212; A New Publishing Solution for News Organizations")

expect.fulfill()
}

waitForExpectations(timeout: timeout, handler: nil)
}

}