From d8a3668d4370e9f3a00ed76184a2e1fced4c1e68 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 00:06:42 +0700 Subject: [PATCH 01/13] Add BloggingPrompt model to CoreData --- .../Models/BloggingPrompt+CoreDataClass.swift | 32 + .../BloggingPrompt+CoreDataProperties.swift | 34 + .../Services/BloggingPromptsService.swift | 8 +- .../WordPress.xcdatamodeld/.xccurrentversion | 2 +- .../WordPress 140.xcdatamodel/contents | 1076 +++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 16 +- 6 files changed, 1162 insertions(+), 6 deletions(-) create mode 100644 WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift create mode 100644 WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift create mode 100644 WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift new file mode 100644 index 000000000000..16054e5ca12d --- /dev/null +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -0,0 +1,32 @@ +import Foundation +import CoreData +import WordPressKit + +public class BloggingPrompt: NSManagedObject { + + enum AttributionSource: String { + case none + case dayOne = "dayone" + + init(with stringValue: String) { + self = AttributionSource(rawValue: stringValue) ?? .none + } + } + + var attributionSource: AttributionSource { + .init(with: attribution) + } + + func configure(with remotePrompt: RemoteBloggingPrompt, for siteID: Int32) { + self.promptID = Int32(remotePrompt.promptID) + self.siteID = siteID + self.text = remotePrompt.text + self.title = remotePrompt.title + self.content = remotePrompt.content + self.attribution = remotePrompt.attribution + self.date = remotePrompt.date + self.answered = remotePrompt.answered + self.answerCount = Int32(remotePrompt.answeredUsersCount) + self.displayAvatarURLs = remotePrompt.answeredUserAvatarURLs + } +} diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift new file mode 100644 index 000000000000..b42fcbbd7985 --- /dev/null +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -0,0 +1,34 @@ +import Foundation +import CoreData + +extension BloggingPrompt { + /// The unique ID for the prompt, received from the server. + @NSManaged public var promptID: Int32 + + /// The site ID for the prompt. + @NSManaged public var siteID: Int32 + + /// The prompt content to be displayed at entry points. + @NSManaged public var text: String + + /// Template title for the draft post. + @NSManaged public var title: String + + /// Template content for the draft post. + @NSManaged public var content: String + + /// The source of the prompt. See `BloggingPrompt.AttributionSource`. + @NSManaged public var attribution: String + + /// The prompt date. Time information should be ignored. + @NSManaged public var date: Date? + + /// Whether the current user has answered the prompt in `siteID`. + @NSManaged public var answered: Bool + + /// The number of users that has answered the prompt. + @NSManaged public var answerCount: Int32 + + /// Contains avatar URLs of some users that have answered the prompt. + @NSManaged public var displayAvatarURLs: [URL]? +} diff --git a/WordPress/Classes/Services/BloggingPromptsService.swift b/WordPress/Classes/Services/BloggingPromptsService.swift index a84a8f1d1b53..05ff6586039d 100644 --- a/WordPress/Classes/Services/BloggingPromptsService.swift +++ b/WordPress/Classes/Services/BloggingPromptsService.swift @@ -28,7 +28,7 @@ class BloggingPromptsService { switch result { case .success(let remotePrompts): // TODO: Upsert into CoreData once the CoreData model is available. - success(remotePrompts.map { BloggingPrompt(with: $0) }) + success([]) case .failure(let error): failure(error) } @@ -64,7 +64,7 @@ class BloggingPromptsService { // MARK: - Temporary model object /// TODO: This is a temporary model to be replaced with Core Data model once the fields have all been finalized. -struct BloggingPrompt { +struct BloggingPromptDAO { let promptID: Int let text: String let title: String // for post title @@ -75,7 +75,7 @@ struct BloggingPrompt { let displayAvatarURLs: [URL] let attribution: String - static let examplePrompt = BloggingPrompt( + static let examplePrompt = BloggingPromptDAO( promptID: 239, text: "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", title: "Prompt number 1", @@ -88,7 +88,7 @@ struct BloggingPrompt { ) } -extension BloggingPrompt { +extension BloggingPromptDAO { init(with remotePrompt: RemoteBloggingPrompt) { promptID = remotePrompt.promptID text = remotePrompt.text diff --git a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion index a6087e972746..69ab7147a303 100644 --- a/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion +++ b/WordPress/Classes/WordPress.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - WordPress 139.xcdatamodel + WordPress 140.xcdatamodel diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents new file mode 100644 index 000000000000..0e6bfe359d33 --- /dev/null +++ b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents @@ -0,0 +1,1076 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 287cee6963d7..844b86f32234 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -4564,6 +4564,10 @@ FAFF153D1C98962E007D1C90 /* SiteSettingsViewController+SiteManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAFF153C1C98962E007D1C90 /* SiteSettingsViewController+SiteManagement.swift */; }; FD21397F13128C5300099582 /* libiconv.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FD21397E13128C5300099582 /* libiconv.dylib */; }; FD3D6D2C1349F5D30061136A /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD3D6D2B1349F5D30061136A /* ImageIO.framework */; }; + FE003F5D282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */; }; + FE003F5E282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */; }; + FE003F5F282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */; }; + FE003F60282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */; }; FE02F95F269DC14A00752A44 /* Comment+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE02F95E269DC14A00752A44 /* Comment+Interface.swift */; }; FE06AC8326C3BD0900B69DE4 /* ShareAppContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */; }; FE06AC8526C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */; }; @@ -7841,6 +7845,9 @@ FD3D6D2B1349F5D30061136A /* ImageIO.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; FDCB9A89134B75B900E5C776 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; FDFB011916B1EA1C00F589A8 /* WordPress 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 10.xcdatamodel"; sourceTree = ""; }; + FE003F54282D48E4006F8D1D /* WordPress 140.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 140.xcdatamodel"; sourceTree = ""; }; + FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataClass.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift"; sourceTree = ""; }; + FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataProperties.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift"; sourceTree = ""; }; FE02F95E269DC14A00752A44 /* Comment+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Interface.swift"; sourceTree = ""; }; FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppContentPresenter.swift; sourceTree = ""; }; FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSource.swift; sourceTree = ""; }; @@ -9019,6 +9026,8 @@ 5D42A3D8175E7452005CFF05 /* BasePost.h */, 5D42A3D9175E7452005CFF05 /* BasePost.m */, E19B17AD1E5C6944007517C6 /* BasePost.swift */, + FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */, + FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */, 98AA6D1026B8CE7200920C8B /* Comment+CoreDataClass.swift */, 9815D0B226B49A0600DF7226 /* Comment+CoreDataProperties.swift */, E14932B4130427B300154804 /* Coordinate.h */, @@ -18006,6 +18015,7 @@ 32E1BFDA24A66F2A007A08F0 /* ReaderInterestsCollectionViewFlowLayout.swift in Sources */, FEC3B81726C2915A00A395C7 /* SingleButtonTableViewCell.swift in Sources */, 836498C828172C5900A2C170 /* WPStyleGuide+BloggingPrompts.swift in Sources */, + FE003F5E282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */, FF70A3221FD5840500BC270D /* PHAsset+Metadata.swift in Sources */, 5DA5BF4418E32DCF005F11F9 /* Theme.m in Sources */, D8A3A5B1206A49A100992576 /* StockPhotosMediaGroup.swift in Sources */, @@ -18280,6 +18290,7 @@ D8A3A5AF206A442800992576 /* StockPhotosDataSource.swift in Sources */, 8B24C4E3249A4C3E0005E8A5 /* OfflineReaderWebView.swift in Sources */, 931215E6267F5192008C3B69 /* ReferrerDetailsViewModel.swift in Sources */, + FE003F60282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */, 9A5C854822B3E42800BEE7A3 /* CountriesMapCell.swift in Sources */, B5B410B61B1772B000CFCF8D /* NavigationTitleView.swift in Sources */, 8BAD53D6241922B900230F4B /* WPAnalyticsEvent.swift in Sources */, @@ -21000,6 +21011,7 @@ FABB24352602FC2C00C8785C /* ErrorStateViewController.swift in Sources */, FABB24362602FC2C00C8785C /* WP3DTouchShortcutCreator.swift in Sources */, FABB24372602FC2C00C8785C /* GIFPlaybackStrategy.swift in Sources */, + FE003F5F282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */, FABB24382602FC2C00C8785C /* ReaderCommentAction.swift in Sources */, FABB24392602FC2C00C8785C /* JetpackBackupStatusFailedViewController.swift in Sources */, FABB243A2602FC2C00C8785C /* CircularImageView.swift in Sources */, @@ -21056,6 +21068,7 @@ 8BE8545E27DBAFED00E4C69A /* BlogDashboardAB.swift in Sources */, FABB24632602FC2C00C8785C /* PushAuthenticationService.swift in Sources */, FABB24642602FC2C00C8785C /* AlertView.swift in Sources */, + FE003F5D282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */, FABB24652602FC2C00C8785C /* WPTabBarController.m in Sources */, FABB24662602FC2C00C8785C /* UIAlertControllerProxy.m in Sources */, FABB24672602FC2C00C8785C /* StatsBarChartView.swift in Sources */, @@ -25951,6 +25964,7 @@ E125443B12BF5A7200D87A0A /* WordPress.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + FE003F54282D48E4006F8D1D /* WordPress 140.xcdatamodel */, 17870A72281847D500D1C627 /* WordPress 139.xcdatamodel */, FE59DA9527D1FD0700624D26 /* WordPress 138.xcdatamodel */, FE97BC13274FCE7A00CF08F9 /* WordPress 137.xcdatamodel */, @@ -26091,7 +26105,7 @@ 8350E15911D28B4A00A7B073 /* WordPress.xcdatamodel */, E125443D12BF5A7200D87A0A /* WordPress 2.xcdatamodel */, ); - currentVersion = 17870A72281847D500D1C627 /* WordPress 139.xcdatamodel */; + currentVersion = FE003F54282D48E4006F8D1D /* WordPress 140.xcdatamodel */; name = WordPress.xcdatamodeld; path = Classes/WordPress.xcdatamodeld; sourceTree = ""; From b298b5c2b2da57a9e8038e5aa6e680ec106efda6 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 01:19:43 +0700 Subject: [PATCH 02/13] Remove attribution enum mapping in favor of #18574 --- .../Models/BloggingPrompt+CoreDataClass.swift | 19 +++++-------------- .../BloggingPrompt+CoreDataProperties.swift | 2 +- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index 16054e5ca12d..87a5a74455ee 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -3,20 +3,11 @@ import CoreData import WordPressKit public class BloggingPrompt: NSManagedObject { - - enum AttributionSource: String { - case none - case dayOne = "dayone" - - init(with stringValue: String) { - self = AttributionSource(rawValue: stringValue) ?? .none - } - } - - var attributionSource: AttributionSource { - .init(with: attribution) - } - + /// Convenience method to map properties from `RemoteBloggingPrompt`. + /// + /// - Parameters: + /// - remotePrompt: The remote prompt model to convert + /// - siteID: The ID of the site that the prompt is intended for func configure(with remotePrompt: RemoteBloggingPrompt, for siteID: Int32) { self.promptID = Int32(remotePrompt.promptID) self.siteID = siteID diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift index b42fcbbd7985..8e0f4e628504 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -17,7 +17,7 @@ extension BloggingPrompt { /// Template content for the draft post. @NSManaged public var content: String - /// The source of the prompt. See `BloggingPrompt.AttributionSource`. + /// The attribution source for the prompt. @NSManaged public var attribution: String /// The prompt date. Time information should be ignored. From 41b6aa99d072170324776bc87b43f0e49db91728 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 16:33:17 +0700 Subject: [PATCH 03/13] Replace temporary model with core data model. - Upsert remote prompts to Core Data. - Adjust prompts card cell to display actual today's prompt. - Adjust prompts intro presenter to show actual today's prompt. - Adjust CreateButtonCoordinator to show local today's prompt. --- .../Models/BloggingPrompt+CoreDataClass.swift | 9 + .../Services/BloggingPromptsService.swift | 167 +++++++++++++----- .../Prompts/DashboardPromptsCardCell.swift | 7 +- ...BloggingPromptsIntroductionPresenter.swift | 33 ++-- .../CreateButtonCoordinator.swift | 7 +- 5 files changed, 162 insertions(+), 61 deletions(-) diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index 87a5a74455ee..f571f66d5148 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -3,6 +3,15 @@ import CoreData import WordPressKit public class BloggingPrompt: NSManagedObject { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: Self.classNameWithoutNamespaces()) + } + + @nonobjc public class func newObject(in context: NSManagedObjectContext) -> BloggingPrompt? { + return NSEntityDescription.insertNewObject(forEntityName: Self.classNameWithoutNamespaces(), into: context) as? BloggingPrompt + } + /// Convenience method to map properties from `RemoteBloggingPrompt`. /// /// - Parameters: diff --git a/WordPress/Classes/Services/BloggingPromptsService.swift b/WordPress/Classes/Services/BloggingPromptsService.swift index 05ff6586039d..5c7f1b2f54d9 100644 --- a/WordPress/Classes/Services/BloggingPromptsService.swift +++ b/WordPress/Classes/Services/BloggingPromptsService.swift @@ -2,13 +2,25 @@ import CoreData import WordPressKit class BloggingPromptsService { - private let context: NSManagedObjectContext + private let contextManager: CoreDataStack private let siteID: NSNumber private let remote: BloggingPromptsServiceRemote private let calendar: Calendar = .autoupdatingCurrent - private var defaultDate: Date { - calendar.date(byAdding: .day, value: -10, to: Date()) ?? Date() + /// A UTC date formatter that ignores time information. + private static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = .init(identifier: "en_US_POSIX") + formatter.timeZone = .init(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd" + + return formatter + }() + + /// Convenience computed variable that returns today's prompt from local store. + /// + var localTodaysPrompt: BloggingPrompt? { + loadPrompts(from: Date(), number: 1).first } /// Fetches a number of blogging prompts starting from the specified date. @@ -23,12 +35,18 @@ class BloggingPromptsService { number: Int = 24, success: @escaping ([BloggingPrompt]) -> Void, failure: @escaping (Error?) -> Void) { - let fromDate = date ?? defaultDate + let fromDate = date ?? defaultStartDate remote.fetchPrompts(for: siteID, number: number, fromDate: fromDate) { result in switch result { case .success(let remotePrompts): - // TODO: Upsert into CoreData once the CoreData model is available. - success([]) + self.upsert(with: remotePrompts) { innerResult in + if case .failure(let error) = innerResult { + failure(error) + return + } + + success(self.loadPrompts(from: fromDate, number: number)) + } case .failure(let error): failure(error) } @@ -47,57 +65,114 @@ class BloggingPromptsService { }, failure: failure) } - required init?(context: NSManagedObjectContext = ContextManager.shared.mainContext, + required init?(contextManager: CoreDataStack = ContextManager.shared, remote: BloggingPromptsServiceRemote? = nil, blog: Blog? = nil) { - guard let account = AccountService(managedObjectContext: context).defaultWordPressComAccount(), + guard let account = AccountService(managedObjectContext: contextManager.mainContext).defaultWordPressComAccount(), let siteID = blog?.dotComID ?? account.primaryBlogID else { return nil } - self.context = context + self.contextManager = contextManager self.siteID = siteID self.remote = remote ?? .init(wordPressComRestApi: account.wordPressComRestV2Api) } } -// MARK: - Temporary model object - -/// TODO: This is a temporary model to be replaced with Core Data model once the fields have all been finalized. -struct BloggingPromptDAO { - let promptID: Int - let text: String - let title: String // for post title - let content: String // for post content - let date: Date - let answered: Bool - let answerCount: Int - let displayAvatarURLs: [URL] - let attribution: String - - static let examplePrompt = BloggingPromptDAO( - promptID: 239, - text: "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", - title: "Prompt number 1", - content: "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n", - date: Date(), - answered: false, - answerCount: 5, - displayAvatarURLs: [], - attribution: "" - ) -} +// MARK: - Private Helpers + +private extension BloggingPromptsService { + + var defaultStartDate: Date { + calendar.date(byAdding: .day, value: -10, to: Date()) ?? Date() + } + + /// Converts the given date to UTC and ignores the time information. + /// Example: Given `2022-05-01 03:00:00 UTC-5`, this should return `2022-05-01 00:00:00 UTC`. + /// + /// - Parameter date: The date to convert. + /// - Returns: The UTC date without the time information. + func utcDateIgnoringTime(from date: Date) -> Date? { + let utcDateString = Self.dateFormatter.string(from: date) + return Self.dateFormatter.date(from: utcDateString) + } + + /// Loads local prompts based on the given parameters. + /// + /// - Parameters: + /// - date: When specified, only prompts from the specified date will be returned. + /// - number: The amount of prompts to return. Defaults to 24 when unspecified. + /// - Returns: An array of `BloggingPrompt` objects sorted descending by date. + func loadPrompts(from date: Date, number: Int) -> [BloggingPrompt] { + guard let utcDate = utcDateIgnoringTime(from: date) else { + DDLogError("Error converting date to UTC: \(date)") + return [] + } + + let fetchRequest = BloggingPrompt.fetchRequest() + fetchRequest.predicate = .init(format: "\(#keyPath(BloggingPrompt.siteID)) = %@ AND \(#keyPath(BloggingPrompt.date)) >= %@", siteID, utcDate as NSDate) + fetchRequest.fetchLimit = number + fetchRequest.sortDescriptors = [.init(key: #keyPath(BloggingPrompt.date), ascending: false)] -extension BloggingPromptDAO { - init(with remotePrompt: RemoteBloggingPrompt) { - promptID = remotePrompt.promptID - text = remotePrompt.text - title = remotePrompt.title - content = remotePrompt.content - date = remotePrompt.date - answered = remotePrompt.answered - answerCount = remotePrompt.answeredUsersCount - displayAvatarURLs = remotePrompt.answeredUserAvatarURLs - attribution = remotePrompt.attribution + return (try? self.contextManager.mainContext.fetch(fetchRequest)) ?? [] + } + + /// Find and update existing prompts, or insert new ones if they don't exist. + /// + /// - Parameters: + /// - remotePrompts: An array containing prompts obtained from remote. + /// - completion: Closure to be called after the process completes. Returns an array of prompts when successful. + func upsert(with remotePrompts: [RemoteBloggingPrompt], completion: @escaping (Result) -> Void) { + if remotePrompts.isEmpty { + completion(.success(())) + return + } + + let remoteIDs = Set(remotePrompts.map { Int32($0.promptID) }) + let remotePromptsDictionary = remotePrompts.reduce(into: [Int32: RemoteBloggingPrompt]()) { partialResult, remotePrompt in + partialResult[Int32(remotePrompt.promptID)] = remotePrompt + } + + let predicate = NSPredicate(format: "\(#keyPath(BloggingPrompt.siteID)) = %@ AND \(#keyPath(BloggingPrompt.promptID)) IN %@", siteID, remoteIDs) + let fetchRequest = BloggingPrompt.fetchRequest() + fetchRequest.predicate = predicate + + let derivedContext = contextManager.newDerivedContext() + derivedContext.perform { + do { + // Update existing prompts + var foundExistingIDs = [Int32]() + let results = try derivedContext.fetch(fetchRequest) + results.forEach { prompt in + guard let remotePrompt = remotePromptsDictionary[prompt.promptID] else { + return + } + + foundExistingIDs.append(prompt.promptID) + prompt.configure(with: remotePrompt, for: self.siteID.int32Value) + } + + // Insert new prompts + let newPromptIDs = remoteIDs.subtracting(foundExistingIDs) + newPromptIDs.forEach { newPromptID in + guard let remotePrompt = remotePromptsDictionary[newPromptID], + let newPrompt = BloggingPrompt.newObject(in: derivedContext) else { + return + } + newPrompt.configure(with: remotePrompt, for: self.siteID.int32Value) + } + + self.contextManager.save(derivedContext) { + DispatchQueue.main.async { + completion(.success(())) + } + } + + } catch let error { + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index 7764efdce6e4..0d1c902459ab 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -115,7 +115,7 @@ class DashboardPromptsCardCell: UICollectionViewCell, Reusable { return Constants.exampleAnswerCount } - return prompt?.answerCount ?? 0 + return Int(prompt?.answerCount ?? 0) }() private var answerInfoText: String { @@ -367,11 +367,12 @@ private extension DashboardPromptsCardCell { // MARK: Button actions @objc func answerButtonTapped() { - guard let blog = blog else { + guard let blog = blog, + let prompt = prompt else { return } - let editor = EditPostViewController(blog: blog, prompt: .examplePrompt) + let editor = EditPostViewController(blog: blog, prompt: prompt) editor.modalPresentationStyle = .fullScreen editor.entryPoint = .dashboard presenterViewController?.present(editor, animated: true) diff --git a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift index 185c8a5bdf2c..f4f25d2013c6 100644 --- a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift +++ b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift @@ -35,6 +35,10 @@ class BloggingPromptsIntroductionPresenter: NSObject { (accountSites?.count ?? 0) == 0 }() + private lazy var bloggingPromptsService: BloggingPromptsService? = { + .init(blog: selectedBlog) + }() + // MARK: - Present Feature Introduction func present(from presentingViewController: UIViewController) { @@ -98,21 +102,30 @@ private extension BloggingPromptsIntroductionPresenter { func showPostCreation() { guard let blog = blogToUse(), - let presentingViewController = presentingViewController else { + let presentingViewController = presentingViewController, + let service = bloggingPromptsService else { navigationController.dismiss(animated: true) return } - // TODO: pre-populate post content with prompt from backend instead - // of example prompt - let editor = EditPostViewController(blog: blog, prompt: .examplePrompt) - editor.modalPresentationStyle = .fullScreen - editor.entryPoint = .bloggingPromptsFeatureIntroduction + service.fetchTodaysPrompt { [weak self] prompt in + guard let prompt = prompt else { + // TODO: Show error message? + return + } - navigationController.dismiss(animated: true, completion: { [weak self] in - presentingViewController.present(editor, animated: false) - self?.trackPostEditorShown(blog) - }) + let editor = EditPostViewController(blog: blog, prompt: prompt) + editor.modalPresentationStyle = .fullScreen + editor.entryPoint = .bloggingPromptsFeatureIntroduction + + self?.navigationController.dismiss(animated: true, completion: { + presentingViewController.present(editor, animated: false) + self?.trackPostEditorShown(blog) + }) + + } failure: { _ in + // TODO: Show error message? + } } func showRemindersScheduling() { diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift index b7b923f7d6d7..ba74a141e8e2 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift @@ -60,10 +60,13 @@ import WordPressFlux let headerView = FeatureFlag.bloggingPrompts.enabled ? BloggingPromptsHeaderView.loadFromNib() : nil headerView?.answerPromptHandler = { [weak self] in self?.viewController?.dismiss(animated: true) { - guard let blog = self?.blog else { + guard let blog = self?.blog, + let service = BloggingPromptsService(blog: blog), + let prompt = service.localTodaysPrompt else { return } - let editor = EditPostViewController(blog: blog, prompt: .examplePrompt) + + let editor = EditPostViewController(blog: blog, prompt: prompt) editor.modalPresentationStyle = .fullScreen editor.entryPoint = .bloggingPromptsActionSheetHeader self?.viewController?.present(editor, animated: true) From 362e4dd05e626e7fba341de8e833b3d0ae838391 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 17:23:20 +0700 Subject: [PATCH 04/13] Set date to non-optional for BloggingPrompt --- .../Classes/Models/BloggingPrompt+CoreDataProperties.swift | 2 +- .../WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift index 8e0f4e628504..99f2b26e5d3b 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -21,7 +21,7 @@ extension BloggingPrompt { @NSManaged public var attribution: String /// The prompt date. Time information should be ignored. - @NSManaged public var date: Date? + @NSManaged public var date: Date /// Whether the current user has answered the prompt in `siteID`. @NSManaged public var answered: Bool diff --git a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents index 0e6bfe359d33..1592098f88b9 100644 --- a/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents +++ b/WordPress/Classes/WordPress.xcdatamodeld/WordPress 140.xcdatamodel/contents @@ -223,7 +223,7 @@ - + From 41003b9f519b6b7fe35ae66d6d64a045f34c8e8d Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 20:08:13 +0700 Subject: [PATCH 05/13] Add tests for BloggingPromptsService --- WordPress/WordPress.xcodeproj/project.pbxproj | 4 + .../BloggingPromptsServiceTests.swift | 113 +++++++++++++++--- .../blogging-prompts-fetch-success.json | 30 +++++ 3 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 844b86f32234..ddd436dc5b63 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -4568,6 +4568,7 @@ FE003F5E282D61BA006F8D1D /* BloggingPrompt+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */; }; FE003F5F282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */; }; FE003F60282D61BA006F8D1D /* BloggingPrompt+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */; }; + FE003F62282E73E6006F8D1D /* blogging-prompts-fetch-success.json in Resources */ = {isa = PBXBuildFile; fileRef = FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */; }; FE02F95F269DC14A00752A44 /* Comment+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE02F95E269DC14A00752A44 /* Comment+Interface.swift */; }; FE06AC8326C3BD0900B69DE4 /* ShareAppContentPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */; }; FE06AC8526C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */; }; @@ -7848,6 +7849,7 @@ FE003F54282D48E4006F8D1D /* WordPress 140.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 140.xcdatamodel"; sourceTree = ""; }; FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataClass.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift"; sourceTree = ""; }; FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataProperties.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift"; sourceTree = ""; }; + FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blogging-prompts-fetch-success.json"; sourceTree = ""; }; FE02F95E269DC14A00752A44 /* Comment+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Interface.swift"; sourceTree = ""; }; FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppContentPresenter.swift; sourceTree = ""; }; FE06AC8426C3C2F800B69DE4 /* ShareAppTextActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppTextActivityItemSource.swift; sourceTree = ""; }; @@ -14374,6 +14376,7 @@ 855408871A6F106800DDBD79 /* app-review-prompt-notifications-disabled.json */, F127FFD724213B5600B9D41A /* atomic-get-authentication-cookie-success.json */, 93CD939219099BE70049096E /* authtoken.json */, + FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */, FEFC0F8D27313DCF001F7F1D /* comments-v2-success.json */, FEFC0F8F27315634001F7F1D /* empty-array.json */, 74585B9A1F0D591D00E7E667 /* domain-service-valid-domains.json */, @@ -16444,6 +16447,7 @@ B5AEEC7D1ACACFDA008BF2A4 /* notifications-replied-comment.json in Resources */, 8B2D4F5327ECE089009B085C /* dashboard-200-without-posts.json in Resources */, FEFC0F8E27313DD0001F7F1D /* comments-v2-success.json in Resources */, + FE003F62282E73E6006F8D1D /* blogging-prompts-fetch-success.json in Resources */, 46CFA7BF262745F70077BAD9 /* get_wp_v2_themes_twentytwentyone.json in Resources */, 465F89F7263B690C00F4C950 /* wp-block-editor-v1-settings-success-NotThemeJSON.json in Resources */, 3211055C250C027D0048446F /* valid-gif-header.gif in Resources */, diff --git a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift index d135064e1a11..8db8851fb98c 100644 --- a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift +++ b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift @@ -1,15 +1,22 @@ import XCTest +import OHHTTPStubs @testable import WordPress final class BloggingPromptsServiceTests: XCTestCase { private let siteID = 1 private let timeout: TimeInterval = 2 - private var endpoint: String { - "sites/\(siteID)/blogging-prompts" - } - private var context: NSManagedObjectContext! + private static var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + formatter.locale = .init(identifier: "en_US_POSIX") + formatter.timeZone = .init(secondsFromGMT: 0) + + return formatter + }() + + private var contextManager: TestContextManager! private var remote: BloggingPromptsServiceRemoteMock! private var service: BloggingPromptsService! private var blog: Blog! @@ -18,18 +25,18 @@ final class BloggingPromptsServiceTests: XCTestCase { override func setUp() { super.setUp() - context = TestContextManager().mainContext + contextManager = TestContextManager() remote = BloggingPromptsServiceRemoteMock() blog = makeBlog() accountService = makeAccountService() - service = BloggingPromptsService(context: context, remote: remote, blog: blog) + service = BloggingPromptsService(contextManager: contextManager, remote: remote, blog: blog) } override func tearDown() { - context.reset() ContextManager.overrideSharedInstance(nil) + HTTPStubs.removeAllStubs() - context = nil + contextManager = nil remote = nil blog = nil accountService = nil @@ -40,12 +47,82 @@ final class BloggingPromptsServiceTests: XCTestCase { // MARK: - Tests func test_fetchPrompts_givenSuccessfulResult_callsSuccessBlock() { + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: .init(timeIntervalSince1970: 0)) { prompts in + XCTAssertEqual(prompts.count, 2) + + // Verify mappings for the first prompt + let firstPrompt = prompts.first! + XCTAssertEqual(firstPrompt.promptID, 239) + XCTAssertEqual(firstPrompt.text, "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.") + XCTAssertEqual(firstPrompt.title, "Prompt number 1") + XCTAssertEqual(firstPrompt.content, "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n") + XCTAssertEqual(firstPrompt.attribution, "dayone") + + let firstDateComponents = firstPrompt.date.dateAndTimeComponents() + XCTAssertEqual(firstDateComponents.year!, 2022) + XCTAssertEqual(firstDateComponents.month!, 5) + XCTAssertEqual(firstDateComponents.day!, 3) + + XCTAssertFalse(firstPrompt.answered) + XCTAssertEqual(firstPrompt.answerCount, 0) + XCTAssertNotNil(firstPrompt.displayAvatarURLs) + XCTAssertTrue(firstPrompt.displayAvatarURLs!.isEmpty) + + // Verify mappings for the second prompt + let secondPrompt = prompts.last! + XCTAssertEqual(secondPrompt.promptID, 248) + XCTAssertEqual(secondPrompt.text, "Tell us about a time when you felt out of place.") + XCTAssertEqual(secondPrompt.title, "Prompt number 10") + XCTAssertEqual(secondPrompt.content, "\n

Tell us about a time when you felt out of place.

(courtesy of plinky.com)
\n") + XCTAssertTrue(secondPrompt.attribution.isEmpty) + + let secondDateComponents = secondPrompt.date.dateAndTimeComponents() + XCTAssertEqual(secondDateComponents.year!, 2021) + XCTAssertEqual(secondDateComponents.month!, 9) + XCTAssertEqual(secondDateComponents.day!, 12) + + XCTAssertTrue(secondPrompt.answered) + XCTAssertEqual(secondPrompt.answerCount, 1) + XCTAssertNotNil(secondPrompt.displayAvatarURLs) + XCTAssertEqual(secondPrompt.displayAvatarURLs!.count, 1) - service.fetchPrompts { _ in - // TODO: Add mapping tests once CoreData model is added. expectation.fulfill() - } failure: { _ in + + } failure: { error in + XCTFail("This closure shouldn't be called.") + expectation.fulfill() + } + + wait(for: [expectation], timeout: timeout) + } + + func test_fetchPrompts_shouldExcludePromptsOutsideGivenDate() { + // this should exclude the second prompt dated 2021-09-12. + let dateParam = Self.dateFormatter.date(from: "2022-01-01") + + // use actual remote object so the request can be intercepted by HTTPStubs. + service = BloggingPromptsService(contextManager: contextManager, blog: blog) + stubFetchPromptsResponse() + + let expectation = expectation(description: "Fetch prompts should succeed") + service.fetchPrompts(from: dateParam) { prompts in + XCTAssertEqual(prompts.count, 1) + + // Ensure that the date returned is more recent than the supplied date parameter. + let firstPrompt = prompts.first! + let firstDateComponents = firstPrompt.date.dateAndTimeComponents() + XCTAssertEqual(firstDateComponents.year!, 2022) + XCTAssertEqual(firstDateComponents.month!, 5) + XCTAssertEqual(firstDateComponents.day!, 3) + + expectation.fulfill() + + } failure: { error in XCTFail("This closure shouldn't be called.") expectation.fulfill() } @@ -105,7 +182,7 @@ final class BloggingPromptsServiceTests: XCTestCase { private extension BloggingPromptsServiceTests { func makeAccountService() -> AccountService { - let service = AccountService(managedObjectContext: context) + let service = AccountService(managedObjectContext: contextManager.mainContext) let account = service.createOrUpdateAccount(withUsername: "testuser", authToken: "authtoken") account.userID = NSNumber(value: 1) service.setDefaultWordPressComAccount(account) @@ -114,7 +191,14 @@ private extension BloggingPromptsServiceTests { } func makeBlog() -> Blog { - return BlogBuilder(context).isHostedAtWPcom().build() + return BlogBuilder(contextManager.mainContext).isHostedAtWPcom().build() + } + + func stubFetchPromptsResponse() { + stub(condition: isMethodGET()) { _ in + let stubPath = OHPathForFile("blogging-prompts-fetch-success.json", type(of: self)) + return fixture(filePath: stubPath!, headers: ["Content-Type": "application/json"]) + } } } @@ -123,6 +207,7 @@ class BloggingPromptsServiceRemoteMock: BloggingPromptsServiceRemote { var passedNumberParameter: Int? = nil var passedDateParameter: Date? = nil var shouldReturnSuccess: Bool = true + var promptsToReturn = [RemoteBloggingPrompt]() override func fetchPrompts(for siteID: NSNumber, number: Int? = nil, @@ -133,7 +218,7 @@ class BloggingPromptsServiceRemoteMock: BloggingPromptsServiceRemote { passedDateParameter = fromDate if shouldReturnSuccess { - completion(.success([])) + completion(.success(promptsToReturn)) } else { completion(.failure(Errors.failed)) } diff --git a/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json b/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json new file mode 100644 index 000000000000..f7a8d7ee646d --- /dev/null +++ b/WordPress/WordPressTest/Test Data/blogging-prompts-fetch-success.json @@ -0,0 +1,30 @@ +{ + "prompts": [ + { + "id": 239, + "text": "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", + "title": "Prompt number 1", + "content": "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n", + "attribution": "dayone", + "date": "2022-05-03", + "answered": false, + "answered_users_count": 0, + "answered_users_sample": [] + }, + { + "id": 248, + "text": "Tell us about a time when you felt out of place.", + "title": "Prompt number 10", + "content": "\n

Tell us about a time when you felt out of place.

(courtesy of plinky.com)
\n", + "attribution": "", + "date": "2021-09-12", + "answered": true, + "answered_users_count": 1, + "answered_users_sample": [ + { + "avatar": "https://0.gravatar.com/avatar/example?s=96&d=identicon&r=G" + } + ] + } + ] +} From 132465f5a667fa9eef012fcb44287831e765ae50 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 20:15:39 +0700 Subject: [PATCH 06/13] Assign default value on insert, remove optionals --- WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift | 5 +++++ .../Classes/Models/BloggingPrompt+CoreDataProperties.swift | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index f571f66d5148..788347abf6a0 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -12,6 +12,11 @@ public class BloggingPrompt: NSManagedObject { return NSEntityDescription.insertNewObject(forEntityName: Self.classNameWithoutNamespaces(), into: context) as? BloggingPrompt } + public override func awakeFromInsert() { + self.date = .init(timeIntervalSince1970: 0) + self.displayAvatarURLs = [] + } + /// Convenience method to map properties from `RemoteBloggingPrompt`. /// /// - Parameters: diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift index 99f2b26e5d3b..a40e60d39968 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift @@ -30,5 +30,5 @@ extension BloggingPrompt { @NSManaged public var answerCount: Int32 /// Contains avatar URLs of some users that have answered the prompt. - @NSManaged public var displayAvatarURLs: [URL]? + @NSManaged public var displayAvatarURLs: [URL] } From 05f456b633b292060c7306f5fc40d3bde3db5fab Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 20:52:15 +0700 Subject: [PATCH 07/13] Remove examplePrompt references --- .../System/Floating Create Button/CreateButtonCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift index 885a800d9e7e..8705aeeef9f1 100644 --- a/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Floating Create Button/CreateButtonCoordinator.swift @@ -334,7 +334,7 @@ private extension CreateButtonCoordinator { promptsHeaderView.answerPromptHandler = { [weak self] in self?.viewController?.dismiss(animated: true) { // TODO: pass prompt to post editor - let editor = EditPostViewController(blog: blog, prompt: .examplePrompt) + let editor = EditPostViewController(blog: blog, prompt: prompt) editor.modalPresentationStyle = .fullScreen editor.entryPoint = .bloggingPromptsActionSheetHeader self?.viewController?.present(editor, animated: true) From da88115a2712d21d9e3a670453185d4d1a7a79de Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 20:59:00 +0700 Subject: [PATCH 08/13] Add convenience method that returns BloggingPromptsAttribution --- WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift | 4 ++++ .../Cards/Prompts/DashboardPromptsCardCell.swift | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift index 788347abf6a0..6f59f71b39c2 100644 --- a/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift +++ b/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift @@ -17,6 +17,10 @@ public class BloggingPrompt: NSManagedObject { self.displayAvatarURLs = [] } + var promptAttribution: BloggingPromptsAttribution? { + BloggingPromptsAttribution(rawValue: attribution.lowercased()) + } + /// Convenience method to map properties from `RemoteBloggingPrompt`. /// /// - Parameters: diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift index 0c56891ba5d9..c86243ebacc1 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardPromptsCardCell.swift @@ -352,8 +352,7 @@ private extension DashboardPromptsCardCell { promptLabel.text = forExampleDisplay ? Strings.examplePrompt : prompt?.text.stringByDecodingXMLCharacters().trim() containerStackView.addArrangedSubview(promptTitleView) - if let promptAttribution = prompt?.attribution.lowercased(), - let attribution = BloggingPromptsAttribution(rawValue: promptAttribution) { + if let attribution = prompt?.promptAttribution { attributionIcon.image = attribution.iconImage attributionSourceLabel.attributedText = attribution.attributedText containerStackView.addArrangedSubview(attributionStackView) From 0f763d8a5bf6eddf0427c639a15c545962d178ce Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 21:04:47 +0700 Subject: [PATCH 09/13] Fix tests --- WordPress/WordPressTest/BloggingPromptsServiceTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift index 76cd255044a2..09ae22bbbdc3 100644 --- a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift +++ b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift @@ -73,8 +73,7 @@ final class BloggingPromptsServiceTests: XCTestCase { XCTAssertFalse(firstPrompt.answered) XCTAssertEqual(firstPrompt.answerCount, 0) - XCTAssertNotNil(firstPrompt.displayAvatarURLs) - XCTAssertTrue(firstPrompt.displayAvatarURLs!.isEmpty) + XCTAssertTrue(firstPrompt.displayAvatarURLs.isEmpty) // Verify mappings for the second prompt let secondPrompt = prompts.last! @@ -91,8 +90,7 @@ final class BloggingPromptsServiceTests: XCTestCase { XCTAssertTrue(secondPrompt.answered) XCTAssertEqual(secondPrompt.answerCount, 1) - XCTAssertNotNil(secondPrompt.displayAvatarURLs) - XCTAssertEqual(secondPrompt.displayAvatarURLs!.count, 1) + XCTAssertEqual(secondPrompt.displayAvatarURLs.count, 1) expectation.fulfill() From 800151f2bc18659ba0cbe064913ac98deae74b10 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 21:06:06 +0700 Subject: [PATCH 10/13] Remove unused line --- .../BloggingPromptsIntroductionPresenter.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift index cbe6ef32bd95..2c9e0ef24abe 100644 --- a/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift +++ b/WordPress/Classes/ViewRelated/Feature Introduction/Blogging Prompts/BloggingPromptsIntroductionPresenter.swift @@ -102,8 +102,7 @@ private extension BloggingPromptsIntroductionPresenter { func showPostCreation() { guard let blog = blogToUse(), - let presentingViewController = presentingViewController, - let service = bloggingPromptsService else { + let presentingViewController = presentingViewController else { navigationController.dismiss(animated: true) return } From e459de2a7cd92b7d56b518a220c1a6dc42022b83 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Fri, 13 May 2022 23:06:35 +0700 Subject: [PATCH 11/13] Change file settings for BloggingPrompt generated files --- WordPress/WordPress.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index b1d2cd57e28d..ce903847b7c7 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -7852,8 +7852,8 @@ FDCB9A89134B75B900E5C776 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; FDFB011916B1EA1C00F589A8 /* WordPress 10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 10.xcdatamodel"; sourceTree = ""; }; FE003F54282D48E4006F8D1D /* WordPress 140.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 140.xcdatamodel"; sourceTree = ""; }; - FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataClass.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataClass.swift"; sourceTree = ""; }; - FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "BloggingPrompt+CoreDataProperties.swift"; path = "/Users/dvdchr/code/wp-ios/WordPress/Classes/Models/BloggingPrompt+CoreDataProperties.swift"; sourceTree = ""; }; + FE003F5B282D61B9006F8D1D /* BloggingPrompt+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BloggingPrompt+CoreDataClass.swift"; sourceTree = ""; }; + FE003F5C282D61B9006F8D1D /* BloggingPrompt+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BloggingPrompt+CoreDataProperties.swift"; sourceTree = ""; }; FE003F61282E73E6006F8D1D /* blogging-prompts-fetch-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blogging-prompts-fetch-success.json"; sourceTree = ""; }; FE02F95E269DC14A00752A44 /* Comment+Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Interface.swift"; sourceTree = ""; }; FE06AC8226C3BD0900B69DE4 /* ShareAppContentPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppContentPresenter.swift; sourceTree = ""; }; From e0069018ed807606aebd80f6a19532d29c580df7 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Sat, 14 May 2022 00:03:07 +0700 Subject: [PATCH 12/13] Update MIGRATIONS --- MIGRATIONS.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/MIGRATIONS.md b/MIGRATIONS.md index 6e18871a8873..46ec527531b7 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -3,6 +3,22 @@ This file documents changes in the data model. Please explain any changes to the data model as well as any custom migrations. +## WordPress 140 + +@dvdchr 2022-05-13 + +- Created a new entity `BloggingPrompt` with: + - `promptID` (required, default `0`, `Int 32`) + - `siteID` (required, default `0`, `Int 32`) + - `text` (required, default empty string, `String`) + - `title` (required, default empty string, `String`) + - `content` (required, default empty string, `String`) + - `attribution` (required, default empty string, `String`) + - `date` (optional, no default, `Date`) + - `answered` (required, default `NO`, `Boolean`) + - `answerCount` (required, default `0`, `Int 32`) + - `displayAvatarURLs` (optional, no default, `Transformable` with type `[URL]`) + ## WordPress 138 @dvdchr 2022-03-07 From c3ca0039bdb974bccaef36ce2ff9568c30ee4972 Mon Sep 17 00:00:00 2001 From: David Christiandy <1299411+dvdchr@users.noreply.github.com> Date: Sat, 14 May 2022 02:42:09 +0700 Subject: [PATCH 13/13] Extract date components with UTC timezone instead of local timezone --- .../WordPressTest/BloggingPromptsServiceTests.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift index 09ae22bbbdc3..d421f7a3c37b 100644 --- a/WordPress/WordPressTest/BloggingPromptsServiceTests.swift +++ b/WordPress/WordPressTest/BloggingPromptsServiceTests.swift @@ -7,11 +7,12 @@ final class BloggingPromptsServiceTests: XCTestCase { private let siteID = 1 private let timeout: TimeInterval = 2 + private static let utcTimeZone = TimeZone(secondsFromGMT: 0)! private static var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" formatter.locale = .init(identifier: "en_US_POSIX") - formatter.timeZone = .init(secondsFromGMT: 0) + formatter.timeZone = utcTimeZone return formatter }() @@ -66,7 +67,7 @@ final class BloggingPromptsServiceTests: XCTestCase { XCTAssertEqual(firstPrompt.content, "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n") XCTAssertEqual(firstPrompt.attribution, "dayone") - let firstDateComponents = firstPrompt.date.dateAndTimeComponents() + let firstDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: firstPrompt.date) XCTAssertEqual(firstDateComponents.year!, 2022) XCTAssertEqual(firstDateComponents.month!, 5) XCTAssertEqual(firstDateComponents.day!, 3) @@ -83,7 +84,7 @@ final class BloggingPromptsServiceTests: XCTestCase { XCTAssertEqual(secondPrompt.content, "\n

Tell us about a time when you felt out of place.

(courtesy of plinky.com)
\n") XCTAssertTrue(secondPrompt.attribution.isEmpty) - let secondDateComponents = secondPrompt.date.dateAndTimeComponents() + let secondDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: secondPrompt.date) XCTAssertEqual(secondDateComponents.year!, 2021) XCTAssertEqual(secondDateComponents.month!, 9) XCTAssertEqual(secondDateComponents.day!, 12) @@ -116,7 +117,7 @@ final class BloggingPromptsServiceTests: XCTestCase { // Ensure that the date returned is more recent than the supplied date parameter. let firstPrompt = prompts.first! - let firstDateComponents = firstPrompt.date.dateAndTimeComponents() + let firstDateComponents = Calendar.current.dateComponents(in: Self.utcTimeZone, from: firstPrompt.date) XCTAssertEqual(firstDateComponents.year!, 2022) XCTAssertEqual(firstDateComponents.month!, 5) XCTAssertEqual(firstDateComponents.day!, 3) @@ -155,7 +156,7 @@ final class BloggingPromptsServiceTests: XCTestCase { XCTAssertNotNil(remote.passedDateParameter) let passedDate = remote.passedDateParameter! - let differenceInHours = Calendar.autoupdatingCurrent.dateComponents([.hour], from: passedDate, to: Date()).hour! + let differenceInHours = Calendar.current.dateComponents([.hour], from: passedDate, to: Date()).hour! XCTAssertEqual(differenceInHours, expectedDifferenceInHours) XCTAssertNotNil(remote.passedNumberParameter)