diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 6a87342c60..3a765a40fb 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -48,8 +48,6 @@ 080E1ACC212571D9006B58A9 /* StoryPartViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080E1ACB212571D9006B58A9 /* StoryPartViewFactory.swift */; }; 080E1ACE212583E5006B58A9 /* StoryTemplatesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080E1ACD212583E5006B58A9 /* StoryTemplatesAPI.swift */; }; 080E80F51F0070C900DC0EA5 /* CodeLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080E80F41F0070C900DC0EA5 /* CodeLanguages.swift */; }; - 080EBA331EA64BC000C43C93 /* PresentationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080EBA321EA64BC000C43C93 /* PresentationContainer.swift */; }; - 080EBA371EA64C0C00C43C93 /* CertificatesPresentationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080EBA361EA64C0C00C43C93 /* CertificatesPresentationContainer.swift */; }; 080F211B2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F211A2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift */; }; 080F31DD1BA7162C00F356A0 /* StepikToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F31DC1BA7162C00F356A0 /* StepikToken.swift */; }; 081387E11D7AF7700092E05D /* StyledTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081387E01D7AF7700092E05D /* StyledTabBarViewController.swift */; }; @@ -2009,8 +2007,6 @@ 080E1ACB212571D9006B58A9 /* StoryPartViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryPartViewFactory.swift; sourceTree = ""; }; 080E1ACD212583E5006B58A9 /* StoryTemplatesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTemplatesAPI.swift; sourceTree = ""; }; 080E80F41F0070C900DC0EA5 /* CodeLanguages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeLanguages.swift; sourceTree = ""; }; - 080EBA321EA64BC000C43C93 /* PresentationContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationContainer.swift; sourceTree = ""; }; - 080EBA361EA64C0C00C43C93 /* CertificatesPresentationContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificatesPresentationContainer.swift; sourceTree = ""; }; 080F211A2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalProgressLastViewedUpdater.swift; sourceTree = ""; }; 080F31DC1BA7162C00F356A0 /* StepikToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepikToken.swift; sourceTree = ""; }; 081387E01D7AF7700092E05D /* StyledTabBarViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledTabBarViewController.swift; sourceTree = ""; }; @@ -7332,7 +7328,6 @@ children = ( 2CFF8FF6242A1E9F00FD7311 /* Defaults */, 2CFF8FF7242A1EAA00FD7311 /* Preferences */, - 2CFF8FF8242A1EB800FD7311 /* Presentation */, ); path = Containers; sourceTree = ""; @@ -7356,15 +7351,6 @@ path = Preferences; sourceTree = ""; }; - 2CFF8FF8242A1EB800FD7311 /* Presentation */ = { - isa = PBXGroup; - children = ( - 080EBA361EA64C0C00C43C93 /* CertificatesPresentationContainer.swift */, - 080EBA321EA64BC000C43C93 /* PresentationContainer.swift */, - ); - path = Presentation; - sourceTree = ""; - }; 2CFF8FF9242A228C00FD7311 /* TransitionRouters */ = { isa = PBXGroup; children = ( @@ -11479,7 +11465,6 @@ 2C22042720E277E50060117A /* SkeletonView.swift in Sources */, 08F555651C4FF22600C877E8 /* QuizControllerDelegate.swift in Sources */, 0891424B1BCEE4EF0000BCB0 /* VideoURL.swift in Sources */, - 080EBA371EA64C0C00C43C93 /* CertificatesPresentationContainer.swift in Sources */, 089877A0214047650065DFA2 /* SplitTestGroupProtocol.swift in Sources */, 2CF08864205BEF3C00FCB9C0 /* StepikPlaceholderView.swift in Sources */, 2C6E9CDB1FF27543001821A2 /* AdaptiveStorageManager.swift in Sources */, @@ -11587,7 +11572,6 @@ 2C2F0BE92186EF87007DCA0A /* CommonNotificationsRequestAlertDataSource.swift in Sources */, 086BE2851F901CCF00B4BE56 /* LastStepRouter.swift in Sources */, 2CECFD5526861FAA00849590 /* CourseBenefitDetailOutputProtocol.swift in Sources */, - 080EBA331EA64BC000C43C93 /* PresentationContainer.swift in Sources */, 084DDDE1204EDF7600913503 /* NotificationRequestAlertViewController.swift in Sources */, 2C2485472101D91F006F8858 /* RestorableBackgroundDownloaderProtocol.swift in Sources */, 2CE9BF46248D08A8004F6659 /* CodeSamplesPersistenceService.swift in Sources */, diff --git a/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.swift b/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.swift index bd32cec910..71f79476c5 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.swift @@ -1,27 +1,30 @@ -// -// CertificateTableViewCell.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import UIKit final class CertificateTableViewCell: UITableViewCell { + enum Appearance { + static let editButtonTopOffset: CGFloat = 8 + + static let actionButtonHeight: CGFloat = 31 + } + @IBOutlet weak var courseTitle: StepikLabel! @IBOutlet weak var certificateDescription: StepikLabel! @IBOutlet weak var certificateResult: StepikLabel! @IBOutlet weak var shareButton: UIButton! @IBOutlet weak var courseImage: UIImageView! + @IBOutlet var editButton: UIButton! + @IBOutlet var editButtonTopConstraint: NSLayoutConstraint! + @IBOutlet var editButtonHeightConstraint: NSLayoutConstraint! private var viewData: CertificateViewData? var shareBlock: ((CertificateViewData, UIButton) -> Void)? + var editBlock: ((CertificateViewData, UIButton) -> Void)? override func awakeFromNib() { super.awakeFromNib() self.shareButton.setTitle(NSLocalizedString("Share", comment: ""), for: .normal) + self.editButton.setTitle(NSLocalizedString("CertificateNameChangeAction", comment: ""), for: .normal) } func initWith(certificateViewData: CertificateViewData) { @@ -33,6 +36,12 @@ final class CertificateTableViewCell: UITableViewCell { url: certificateViewData.courseImageURL, placeholder: Images.lessonPlaceholderImage.size50x50 ) + + self.editButton.isHidden = !certificateViewData.isEditAvailable + self.editButtonTopConstraint.constant = self.editButton.isHidden ? 0 : Appearance.editButtonTopOffset + self.editButtonHeightConstraint.constant = self.editButton.isHidden + ? 0 + : Appearance.actionButtonHeight } @IBAction @@ -41,4 +50,14 @@ final class CertificateTableViewCell: UITableViewCell { self.shareBlock?(viewData, sender) } } + + @IBAction + func editPressed(_ sender: UIButton) { + guard let viewData = self.viewData, + viewData.isEditAvailable else { + return + } + + self.editBlock?(viewData, sender) + } } diff --git a/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.xib b/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.xib index a8898bebdb..1c5f425afc 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.xib +++ b/Stepic/Legacy/Controllers/Certificates/CertificateTableViewCell/CertificateTableViewCell.xib @@ -1,46 +1,46 @@ - + - + - - + + - - + + - - + + - - + + + - - + + + + + + - - - + - @@ -80,9 +95,23 @@ + + + - + + + + + + + + + + + + diff --git a/Stepic/Legacy/Controllers/Certificates/CertificateViewData.swift b/Stepic/Legacy/Controllers/Certificates/CertificateViewData.swift index 8ce0b8fc78..f2d8bb4d21 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificateViewData.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificateViewData.swift @@ -1,17 +1,16 @@ -// -// CertificateViewData.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import Foundation -struct CertificateViewData { +struct CertificateViewData: UniqueIdentifiable { + let uniqueIdentifier: UniqueIdentifierType + let courseName: String? let courseImageURL: URL? let grade: Int let certificateURL: URL? let certificateDescription: String? + + let isEditAvailable: Bool + let editsCount: Int + let allowedEditsCount: Int + let savedFullName: String } diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesPresenter.swift b/Stepic/Legacy/Controllers/Certificates/CertificatesPresenter.swift index 6f1080c62b..fd0d93c44c 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesPresenter.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesPresenter.swift @@ -1,113 +1,137 @@ -// -// CertificatesPresenter.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import Foundation +import PromiseKit final class CertificatesPresenter { weak var view: CertificatesView? private let userID: User.IdType - private let certificatesAPI: CertificatesAPI - private let coursesAPI: CoursesAPI - private let presentationContainer: CertificatesPresentationContainer + private let certificatesNetworkService: CertificatesNetworkServiceProtocol private let certificatesPersistenceService: CertificatesPersistenceServiceProtocol - private var certificates: [Certificate] = [] { - didSet { - self.updatePersistentPresentationData() - } - } + private let coursesNetworkService: CoursesNetworkServiceProtocol - private var page = 1 - private var isGettingNextPage = false + private let userAccountService: UserAccountServiceProtocol + + private var currentCertificates = [Certificate]() + private var currentPage = 1 + private var isFetchingNextPage = false init( userID: User.IdType, - certificatesAPI: CertificatesAPI, - coursesAPI: CoursesAPI, - presentationContainer: CertificatesPresentationContainer, + certificatesNetworkService: CertificatesNetworkServiceProtocol, certificatesPersistenceService: CertificatesPersistenceServiceProtocol, + coursesNetworkService: CoursesNetworkServiceProtocol, + userAccountService: UserAccountServiceProtocol, view: CertificatesView? ) { self.userID = userID - self.certificatesAPI = certificatesAPI - self.coursesAPI = coursesAPI - self.presentationContainer = presentationContainer + self.certificatesNetworkService = certificatesNetworkService self.certificatesPersistenceService = certificatesPersistenceService + self.coursesNetworkService = coursesNetworkService + self.userAccountService = userAccountService self.view = view } + // MARK: Public API + func getCachedCertificates() { - let localIds = self.presentationContainer.certificatesIds - - self.certificatesPersistenceService.fetch(ids: localIds, userID: self.userID).done { cachedCertificates in - let localCertificates = cachedCertificates.sorted(by: { - guard let index1 = localIds.firstIndex(of: $0.id), - let index2 = localIds.firstIndex(of: $1.id) else { - return false - } - return index1 < index2 - }).compactMap { [weak self] in - self?.makeViewData(from: $0) + self.certificatesPersistenceService.fetch(userID: self.userID).done { cachedCertificates in + if cachedCertificates.isEmpty { + return } - self.view?.setCertificates(certificates: localCertificates, hasNextPage: false) + let viewData = cachedCertificates.map(self.makeViewData(from:)) + self.view?.setCertificates(certificates: viewData, hasNextPage: false) } } - private func updatePersistentPresentationData() { - self.presentationContainer.certificatesIds = certificates.map { $0.id } - } - func refreshCertificates() { - view?.displayRefreshing() + self.view?.displayRefreshing() + + self.certificatesNetworkService.fetch( + userID: self.userID + ).then { fetchResult -> Promise<([Certificate], Meta)> in + self.loadCoursesForCertificates(fetchResult.0).map { fetchResult } + }.done { certificates, meta in + self.currentCertificates = certificates + self.currentPage = 1 + + let viewData = self.currentCertificates.map(self.makeViewData(from:)) + self.view?.setCertificates(certificates: viewData, hasNextPage: meta.hasNext) + }.catch { _ in + self.view?.displayError() + } + } - self.certificatesAPI.retrieve(userId: self.userID, success: { [weak self] meta, newCertificates in - self?.certificates = newCertificates - self?.page = 1 + func getNextPage() -> Bool { + if self.isFetchingNextPage { + return false + } - self?.loadCoursesForCertificates(certificates: newCertificates, completion: { [weak self] in - guard let strongSelf = self else { - return - } + self.isFetchingNextPage = true + let nextPageIndex = self.currentPage + 1 + + self.certificatesNetworkService.fetch( + userID: self.userID, + page: nextPageIndex + ).then { fetchResult -> Promise<([Certificate], Meta)> in + self.loadCoursesForCertificates(fetchResult.0).map { fetchResult } + }.done { certificates, meta in + self.currentPage = nextPageIndex + self.currentCertificates += certificates + + let viewData = self.currentCertificates.map(self.makeViewData(from:)) + self.view?.setCertificates(certificates: viewData, hasNextPage: meta.hasNext) + }.ensure { + self.isFetchingNextPage = false + }.catch { _ in + self.view?.displayLoadNextPageError() + } - strongSelf.view?.setCertificates(certificates: strongSelf.certificates.compactMap({ [weak self] in - self?.makeViewData(from: $0) - }), hasNextPage: meta.hasNext) - strongSelf.view?.displayEmpty() - CoreDataHelper.shared.save() - }) - }, error: { [weak self] _ in - self?.view?.displayError() - }) + return true } - private func loadCoursesForCertificates(certificates: [Certificate], completion: @escaping () -> Void) { - func matchCoursesToCertificates(courses: [Course]) { - for certificate in certificates { - if let filtered = courses.filter({ $0.id == certificate.courseId }).first { - certificate.course = filtered - } + func updateCertificateName( + viewDataUniqueIdentifier: UniqueIdentifierType, + newFullName: String + ) -> Promise { + guard let certificateEntity = self.currentCertificates.first( + where: { "\($0.id)" == viewDataUniqueIdentifier } + ) else { + return Promise(error: Error.updateCertificateNameFailed) + } + + let oldFullName = certificateEntity.savedFullName + certificateEntity.savedFullName = newFullName + + return Promise { seal in + self.certificatesNetworkService.update(certificate: certificateEntity).done { updatedCertificate in + let viewData = self.makeViewData(from: updatedCertificate) + seal.fulfill(viewData) + }.catch { _ in + certificateEntity.savedFullName = oldFullName + seal.reject(Error.updateCertificateNameFailed) + }.finally { + CoreDataHelper.shared.save() } } + } - let courseIds = certificates.map { $0.courseId } + // MARK: Private API - let localCourses = [Course]() - matchCoursesToCertificates(courses: localCourses) + private func loadCoursesForCertificates(_ certificates: [Certificate]) -> Promise { + self.coursesNetworkService.fetch(ids: certificates.map(\.courseID)).done { courses in + if certificates.isEmpty || courses.isEmpty { + return + } - self.coursesAPI.retrieve(ids: courseIds, existing: localCourses, refreshMode: .update, success: { courses in - matchCoursesToCertificates(courses: courses) - completion() - }, error: { _ in - completion() - }) + let coursesMap = Dictionary(courses.map({ ($0.id, $0) }), uniquingKeysWith: { first, _ in first }) + + for certificate in certificates { + certificate.course = coursesMap[certificate.courseID] + } + } } private func makeViewData(from certificate: Certificate) -> CertificateViewData { @@ -127,44 +151,23 @@ final class CertificatesPresenter { let certificateDescriptionString = "\(certificateDescriptionBeginning) \(NSLocalizedString("CertificateDescriptionBody", comment: "")) \(certificate.course?.title ?? "")" + let isEditAvailable = certificate.isEditAllowed && self.userID == self.userAccountService.currentUserID + return CertificateViewData( + uniqueIdentifier: "\(certificate.id)", courseName: certificate.course?.title, courseImageURL: courseImageURL, grade: certificate.grade, certificateURL: certificateURL, - certificateDescription: certificateDescriptionString + certificateDescription: certificateDescriptionString, + isEditAvailable: isEditAvailable, + editsCount: certificate.editsCount, + allowedEditsCount: certificate.allowedEditsCount, + savedFullName: certificate.savedFullName ) } - func getNextPage() -> Bool { - if isGettingNextPage { - return false - } - - isGettingNextPage = true - - self.certificatesAPI.retrieve(userId: self.userID, page: page + 1, success: { - [weak self] meta, newCertificates in - self?.page += 1 - self?.certificates += newCertificates - - self?.loadCoursesForCertificates(certificates: newCertificates, completion: { [weak self] in - guard let strongSelf = self else { - self?.isGettingNextPage = false - return - } - - strongSelf.view?.setCertificates(certificates: strongSelf.certificates.compactMap({ [weak self] in - self?.makeViewData(from: $0) - }), hasNextPage: meta.hasNext) - CoreDataHelper.shared.save() - self?.isGettingNextPage = false - }) - }, error: { [weak self] _ in - self?.view?.displayLoadNextPageError() - self?.isGettingNextPage = false - }) - - return true + enum Error: Swift.Error { + case updateCertificateNameFailed } } diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard b/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard index 24917a880f..61e2873847 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesStoryboard.storyboard @@ -1,9 +1,9 @@ - + - + diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesView.swift b/Stepic/Legacy/Controllers/Certificates/CertificatesView.swift index 6716bf62e0..00ae0f73c2 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesView.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesView.swift @@ -1,11 +1,3 @@ -// -// CertificatesView.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - import Foundation protocol CertificatesView: AnyObject { @@ -15,5 +7,4 @@ protocol CertificatesView: AnyObject { func displayEmpty() func displayRefreshing() func displayLoadNextPageError() - func updateData() } diff --git a/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift b/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift index 2fbc3e7413..dfc963be81 100644 --- a/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift +++ b/Stepic/Legacy/Controllers/Certificates/CertificatesViewController.swift @@ -1,12 +1,5 @@ -// -// CertificatesViewController.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - -import Foundation +import SVProgressHUD +import UIKit @available(*, deprecated, message: "Class to initialize certificates w/o storyboards logic") final class CertificatesLegacyAssembly: Assembly { @@ -27,10 +20,10 @@ final class CertificatesLegacyAssembly: Assembly { certificatesVC.userID = self.userID certificatesVC.presenter = CertificatesPresenter( userID: self.userID, - certificatesAPI: ApiDataDownloader.certificates, - coursesAPI: ApiDataDownloader.courses, - presentationContainer: PresentationContainer.certificates, + certificatesNetworkService: CertificatesNetworkService(certificatesAPI: CertificatesAPI()), certificatesPersistenceService: CertificatesPersistenceService(), + coursesNetworkService: CoursesNetworkService(coursesAPI: CoursesAPI()), + userAccountService: UserAccountService(), view: certificatesVC ) certificatesVC.analytics = StepikAnalytics.shared @@ -39,6 +32,8 @@ final class CertificatesLegacyAssembly: Assembly { } } +// MARK: - CertificatesViewController - + final class CertificatesViewController: UIViewController, ControllerWithStepikPlaceholder { var placeholderContainer = StepikPlaceholderControllerContainer() @@ -60,6 +55,9 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl return paginationView }() + private weak var changeCertificateNameTextField: UITextField? + private weak var changeCertificateNameOKAction: UIAlertAction? + var presenter: CertificatesPresenter? var userID: User.IdType? @@ -68,6 +66,12 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl private var certificates: [CertificateViewData] = [] private var showNextPageFooter = false + private var hasLoadedData = false { + didSet { + self.updateEmptySetPlaceholder() + } + } + override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self @@ -77,14 +81,6 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl tableView.sectionHeaderTopPadding = 0 } - let isMe = AuthInfo.shared.userId != nil && self.userID == AuthInfo.shared.userId - if isMe { - self.tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificatesMe) { [weak self] in - self?.tabBarController?.selectedIndex = 1 - } - } else { - self.tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificatesOther) - } self.tableView.loadingPlaceholder = StepikPlaceholder(.emptyCertificatesLoading) registerPlaceholder(placeholder: StepikPlaceholder(.noConnection, action: { [weak self] in @@ -113,8 +109,9 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl presenter?.getCachedCertificates() presenter?.refreshCertificates() - tableView.backgroundColor = .stepikGroupedBackground - tableView.contentInsetAdjustmentBehavior = .never + self.view.backgroundColor = .stepikGroupedBackground + self.tableView.backgroundColor = .stepikGroupedBackground + self.tableView.contentInsetAdjustmentBehavior = .never DispatchQueue.main.async { self.displayRefreshing() @@ -135,6 +132,21 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl } } + private func updateEmptySetPlaceholder() { + if self.hasLoadedData { + let isMe = AuthInfo.shared.userId != nil && self.userID == AuthInfo.shared.userId + if isMe { + self.tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificatesMe) { + TabBarRouter(tab: .catalog()).route() + } + } else { + self.tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificatesOther) + } + } else { + self.tableView.emptySetPlaceholder = nil + } + } + private func shareCertificate(certificate: CertificateViewData, button: UIButton) { guard let url = certificate.certificateURL else { return @@ -169,8 +181,12 @@ final class CertificatesViewController: UIViewController, ControllerWithStepikPl } } +// MARK: - CertificatesViewController: CertificatesView - + extension CertificatesViewController: CertificatesView { func setCertificates(certificates: [CertificateViewData], hasNextPage: Bool) { + self.hasLoadedData = true + self.certificates = certificates self.showNextPageFooter = hasNextPage @@ -202,12 +218,10 @@ extension CertificatesViewController: CertificatesView { func displayLoadNextPageError() { self.paginationView.setError() } - - func updateData() { - tableView.reloadData() - } } +// MARK: - CertificatesViewController: UITableViewDelegate - + extension CertificatesViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { guard showNextPageFooter else { @@ -253,6 +267,8 @@ extension CertificatesViewController: UITableViewDelegate { } } +// MARK: - CertificatesViewController: UITableViewDataSource - + extension CertificatesViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { self.certificates.isEmpty ? 0 : 1 @@ -277,6 +293,9 @@ extension CertificatesViewController: UITableViewDataSource { cell.shareBlock = { [weak self] viewData, button in self?.shareCertificate(certificate: viewData, button: button) } + cell.editBlock = { [weak self] viewData, _ in + self?.promptForChangeCertificateNameInput(certificate: viewData) + } if certificates.count == indexPath.row + 1 && showNextPageFooter { loadNextPage() @@ -285,3 +304,143 @@ extension CertificatesViewController: UITableViewDataSource { return cell } } + +// MARK: - CertificatesViewController (Certificate Change Name) - + +extension CertificatesViewController { + private func promptForChangeCertificateNameInput( + certificate: CertificateViewData, + predefinedNewFullName: String? = nil + ) { + let message = String( + format: NSLocalizedString("CertificateNameChangeAlertMessageWarning", comment: ""), + arguments: [FormatterHelper.timesCount(certificate.allowedEditsCount)] + ) + + let alert = UIAlertController( + title: NSLocalizedString("CertificateNameChangeAlertTitle", comment: ""), + message: message, + preferredStyle: .alert + ) + + alert.addTextField() + self.changeCertificateNameTextField = alert.textFields?.first + self.changeCertificateNameTextField?.placeholder = NSLocalizedString( + "CertificateNameChangeAlertTextFieldPlaceholder", + comment: "" + ) + self.changeCertificateNameTextField?.delegate = self + if let predefinedNewFullName = predefinedNewFullName { + self.changeCertificateNameTextField?.text = predefinedNewFullName + } + + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel)) + + let okAction = UIAlertAction( + title: NSLocalizedString("OK", comment: ""), + style: .default, + handler: { [weak self, weak alert] _ in + guard let strongSelf = self, + let strongAlert = alert else { + return + } + + guard let text = strongAlert.textFields?.first?.text?.trimmed(), + !text.isEmpty else { + return SVProgressHUD.showError( + withStatus: NSLocalizedString("CertificateNameChangeEmptyTextFieldErrorMessage", comment: "") + ) + } + + strongSelf.promptForChangeCertificateName(certificate: certificate, newFullName: text) + } + ) + okAction.isEnabled = !(predefinedNewFullName ?? "").trimmed().isEmpty + alert.addAction(okAction) + self.changeCertificateNameOKAction = okAction + + self.present(alert, animated: true) + } + + private func promptForChangeCertificateName(certificate: CertificateViewData, newFullName: String) { + var message = String( + format: NSLocalizedString("CertificateNameChangeAlertMessageConfirmation", comment: ""), + arguments: [certificate.savedFullName, newFullName] + ) + + let isLastEdit = (certificate.editsCount + 1) == certificate.allowedEditsCount + if isLastEdit { + message += "\n\(NSLocalizedString("CertificateNameChangeAlertMessageLastEditWarning", comment: ""))" + } + + let alert = UIAlertController( + title: NSLocalizedString("CertificateNameChangeAlertTitle", comment: ""), + message: message, + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel)) + + alert.addAction( + UIAlertAction( + title: NSLocalizedString("OK", comment: ""), + style: .default, + handler: { [weak self] _ in + guard let strongSelf = self else { + return + } + + SVProgressHUD.show() + + strongSelf.presenter?.updateCertificateName( + viewDataUniqueIdentifier: certificate.uniqueIdentifier, + newFullName: newFullName + ).done { updatedCertificate in + SVProgressHUD.showSuccess( + withStatus: NSLocalizedString("CertificateNameChangeSuccessStatusMessage", comment: "") + ) + + let certificateIndexOrNil = strongSelf.certificates.firstIndex( + where: { $0.uniqueIdentifier == updatedCertificate.uniqueIdentifier } + ) + + if let certificateIndex = certificateIndexOrNil { + strongSelf.certificates[certificateIndex] = updatedCertificate + strongSelf.tableView.reloadData() + } + }.catch { _ in + SVProgressHUD.showError( + withStatus: NSLocalizedString("CertificateNameChangeErrorStatusMessage", comment: "") + ) + strongSelf.promptForChangeCertificateNameInput( + certificate: certificate, + predefinedNewFullName: newFullName + ) + } + } + ) + ) + + self.present(alert, animated: true) + } +} + +// MARK: - CertificatesViewController: UITextFieldDelegate - + +extension CertificatesViewController: UITextFieldDelegate { + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + guard self.changeCertificateNameTextField === textField else { + return true + } + + let stringFromTextField = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) ?? "" + + self.changeCertificateNameOKAction?.isEnabled = !stringFromTextField.trimmed().isEmpty + + return true + } +} diff --git a/Stepic/Legacy/Helpers/Containers/Presentation/CertificatesPresentationContainer.swift b/Stepic/Legacy/Helpers/Containers/Presentation/CertificatesPresentationContainer.swift deleted file mode 100644 index dfa2aaf37c..0000000000 --- a/Stepic/Legacy/Helpers/Containers/Presentation/CertificatesPresentationContainer.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CertificatesPresentationContainer.swift -// Stepic -// -// Created by Ostrenkiy on 18.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - -import Foundation - -final class CertificatesPresentationContainer { - private let defaults = UserDefaults.standard - - private let certificatesStoredKey = "certificatesStoredIdsKey" - - var certificatesIds: [Int] { - get { - if let ids = defaults.object(forKey: certificatesStoredKey) as? [Int] { - return ids - } else { - return [] - } - } - set(value) { - defaults.set(value, forKey: certificatesStoredKey) - defaults.synchronize() - } - } -} diff --git a/Stepic/Legacy/Helpers/Containers/Presentation/PresentationContainer.swift b/Stepic/Legacy/Helpers/Containers/Presentation/PresentationContainer.swift deleted file mode 100644 index c8d6516154..0000000000 --- a/Stepic/Legacy/Helpers/Containers/Presentation/PresentationContainer.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PresentationContainer.swift -// Stepic -// -// Created by Ostrenkiy on 18.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - -import Foundation - -/* - Used to store presentation data - */ -final class PresentationContainer { - static let certificates = CertificatesPresentationContainer() - - private init() {} -} diff --git a/Stepic/Legacy/Model/Entities/Certificate/Certificate+CoreDataProperties.swift b/Stepic/Legacy/Model/Entities/Certificate/Certificate+CoreDataProperties.swift index d30512bd22..1235a3c4b5 100644 --- a/Stepic/Legacy/Model/Entities/Certificate/Certificate+CoreDataProperties.swift +++ b/Stepic/Legacy/Model/Entities/Certificate/Certificate+CoreDataProperties.swift @@ -9,7 +9,17 @@ extension Certificate { @NSManaged var managedUpdateDate: Date? @NSManaged var managedGrade: NSNumber? @NSManaged var managedURL: String? - @NSManaged var managedisPublic: NSNumber? + @NSManaged var managedPreviewURL: String? + @NSManaged var managedIsPublic: NSNumber? + @NSManaged var managedUserRank: NSNumber? + @NSManaged var managedUserRankMax: NSNumber? + @NSManaged var managedLeaderboardSize: NSNumber? + @NSManaged var managedSavedFullName: String + @NSManaged var managedEditsCount: NSNumber + @NSManaged var managedAllowedEditsCount: NSNumber + @NSManaged var managedCourseTitle: String + @NSManaged var managedCourseIsPublic: NSNumber + @NSManaged var managedCourseLanguage: String @NSManaged var managedIsWithScore: NSNumber? @NSManaged var managedCourse: Course? @@ -23,7 +33,7 @@ extension Certificate { } } - var courseId: Int { + var courseID: Int { get { self.managedCourseId?.intValue ?? -1 } @@ -32,7 +42,7 @@ extension Certificate { } } - var userId: Int { + var userID: Int { get { self.managedUserId?.intValue ?? -1 } @@ -89,12 +99,102 @@ extension Certificate { } } + var previewURLString: String? { + get { + self.managedPreviewURL + } + set { + self.managedPreviewURL = newValue + } + } + var isPublic: Bool? { get { - self.managedisPublic?.boolValue ?? false + self.managedIsPublic?.boolValue ?? false + } + set { + self.managedIsPublic = newValue as NSNumber? + } + } + + var userRank: Int? { + get { + self.managedUserRank?.intValue + } + set { + self.managedUserRank = newValue as NSNumber? + } + } + + var userRankMax: Int? { + get { + self.managedUserRankMax?.intValue + } + set { + self.managedUserRankMax = newValue as NSNumber? + } + } + + var leaderboardSize: Int? { + get { + self.managedLeaderboardSize?.intValue + } + set { + self.managedLeaderboardSize = newValue as NSNumber? + } + } + + var savedFullName: String { + get { + self.managedSavedFullName + } + set { + self.managedSavedFullName = newValue + } + } + + var editsCount: Int { + get { + self.managedEditsCount.intValue + } + set { + self.managedEditsCount = NSNumber(value: newValue) + } + } + + var allowedEditsCount: Int { + get { + self.managedAllowedEditsCount.intValue + } + set { + self.managedAllowedEditsCount = NSNumber(value: newValue) + } + } + + var courseTitle: String { + get { + self.managedCourseTitle + } + set { + self.managedCourseTitle = newValue + } + } + + var courseIsPublic: Bool { + get { + self.managedCourseIsPublic.boolValue + } + set { + self.managedCourseIsPublic = NSNumber(value: newValue) + } + } + + var courseLanguage: String { + get { + self.managedCourseLanguage } set { - self.managedisPublic = newValue as NSNumber? + self.managedCourseLanguage = newValue } } diff --git a/Stepic/Legacy/Model/Entities/Certificate/Certificate.swift b/Stepic/Legacy/Model/Entities/Certificate/Certificate.swift index e9ae0cbef8..06823adc1c 100644 --- a/Stepic/Legacy/Model/Entities/Certificate/Certificate.swift +++ b/Stepic/Legacy/Model/Entities/Certificate/Certificate.swift @@ -1,15 +1,40 @@ import CoreData import SwiftyJSON -@objc final class Certificate: NSManagedObject, ManagedObject, IDFetchable { typealias IdType = Int - enum CertificateType: String { - case distinction = "distinction" - case regular = "regular" + static var defaultSortDescriptors: [NSSortDescriptor] { + [NSSortDescriptor(key: #keyPath(managedId), ascending: false)] + } + + var json: JSON { + [ + JSONKey.id.rawValue: self.id, + JSONKey.user.rawValue: self.userID, + JSONKey.course.rawValue: self.courseID, + JSONKey.issueDate.rawValue: Parser.timedateStringFromDate(dateOrNil: self.issueDate) as AnyObject, + JSONKey.updateDate.rawValue: Parser.timedateStringFromDate(dateOrNil: self.updateDate) as AnyObject, + JSONKey.grade.rawValue: self.grade, + JSONKey.type.rawValue: self.type.rawValue, + JSONKey.url.rawValue: self.urlString as AnyObject, + JSONKey.previewURL.rawValue: self.previewURLString as AnyObject, + JSONKey.isPublic.rawValue: self.isPublic as AnyObject, + JSONKey.userRank.rawValue: self.userRank as AnyObject, + JSONKey.userRankMax.rawValue: self.userRankMax as AnyObject, + JSONKey.leaderboardSize.rawValue: self.leaderboardSize as AnyObject, + JSONKey.savedFullName.rawValue: self.savedFullName, + JSONKey.editsCount.rawValue: self.editsCount, + JSONKey.allowedEditsCount.rawValue: self.allowedEditsCount, + JSONKey.courseTitle.rawValue: self.courseTitle, + JSONKey.courseIsPublic.rawValue: self.courseIsPublic, + JSONKey.courseLanguage.rawValue: self.courseLanguage, + JSONKey.isWithScore.rawValue: self.isWithScore + ] } + var isEditAllowed: Bool { self.editsCount < self.allowedEditsCount } + required convenience init(json: JSON) { self.init(entity: Self.entity, insertInto: CoreDataHelper.shared.context) self.update(json: json) @@ -17,17 +42,32 @@ final class Certificate: NSManagedObject, ManagedObject, IDFetchable { func update(json: JSON) { self.id = json[JSONKey.id.rawValue].intValue - self.userId = json[JSONKey.user.rawValue].intValue - self.courseId = json[JSONKey.course.rawValue].intValue + self.userID = json[JSONKey.user.rawValue].intValue + self.courseID = json[JSONKey.course.rawValue].intValue self.issueDate = Parser.dateFromTimedateJSON(json[JSONKey.issueDate.rawValue]) self.updateDate = Parser.dateFromTimedateJSON(json[JSONKey.updateDate.rawValue]) self.grade = json[JSONKey.grade.rawValue].intValue self.type = CertificateType(rawValue: json[JSONKey.type.rawValue].stringValue) ?? .regular self.urlString = json[JSONKey.url.rawValue].string + self.previewURLString = json[JSONKey.previewURL.rawValue].string self.isPublic = json[JSONKey.isPublic.rawValue].bool + self.userRank = json[JSONKey.userRank.rawValue].int + self.userRankMax = json[JSONKey.userRankMax.rawValue].int + self.leaderboardSize = json[JSONKey.leaderboardSize.rawValue].int + self.savedFullName = json[JSONKey.savedFullName.rawValue].stringValue + self.editsCount = json[JSONKey.editsCount.rawValue].intValue + self.allowedEditsCount = json[JSONKey.allowedEditsCount.rawValue].intValue + self.courseTitle = json[JSONKey.courseTitle.rawValue].stringValue + self.courseIsPublic = json[JSONKey.courseIsPublic.rawValue].boolValue + self.courseLanguage = json[JSONKey.courseLanguage.rawValue].stringValue self.isWithScore = json[JSONKey.isWithScore.rawValue].boolValue } + enum CertificateType: String { + case regular + case distinction + } + enum JSONKey: String { case id case user @@ -37,7 +77,17 @@ final class Certificate: NSManagedObject, ManagedObject, IDFetchable { case grade case type case url + case previewURL = "preview_url" case isPublic = "is_public" + case userRank = "user_rank" + case userRankMax = "user_rank_max" + case leaderboardSize = "leaderboard_size" + case savedFullName = "saved_fullname" + case editsCount = "edits_count" + case allowedEditsCount = "allowed_edits_count" + case courseTitle = "course_title" + case courseIsPublic = "course_is_public" + case courseLanguage = "course_language" case isWithScore = "is_with_score" } } diff --git a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_course_acquired_skills_v94.xcdatamodel/contents b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_course_acquired_skills_v94.xcdatamodel/contents index 652b7478a3..f47590d81f 100644 --- a/Stepic/Legacy/Model/Model.xcdatamodeld/Model_course_acquired_skills_v94.xcdatamodel/contents +++ b/Stepic/Legacy/Model/Model.xcdatamodeld/Model_course_acquired_skills_v94.xcdatamodel/contents @@ -71,16 +71,26 @@ + + + + + - + + + + + + @@ -622,7 +632,7 @@ - + diff --git a/Stepic/Legacy/Model/Network/ApiDataDownloader.swift b/Stepic/Legacy/Model/Network/ApiDataDownloader.swift index 9d6276a896..cb18bd06d1 100644 --- a/Stepic/Legacy/Model/Network/ApiDataDownloader.swift +++ b/Stepic/Legacy/Model/Network/ApiDataDownloader.swift @@ -6,7 +6,6 @@ import SwiftyJSON final class ApiDataDownloader { static let attempts = AttemptsAPI() static let auth = AuthAPI() - static let certificates = CertificatesAPI() static let courses = CoursesAPI() static let discussionThreads = DiscussionThreadsAPI() static let enrollments = EnrollmentsAPI() diff --git a/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift b/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift index 005d1629dd..acdbe5ebaa 100644 --- a/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift +++ b/Stepic/Legacy/Model/Network/Endpoints/CertificatesAPI.swift @@ -41,22 +41,13 @@ final class CertificatesAPI: APIEndpoint { ) } - //Cannot move it to extension cause it is used in tests - @available(*, deprecated, message: "Legacy method with callbacks") - @discardableResult - func retrieve( - userId: Int, - page: Int = 1, - headers: HTTPHeaders = AuthInfo.shared.initialHTTPHeaders, - success: @escaping (Meta, [Certificate]) -> Void, - error errorHandler: @escaping (NetworkError) -> Void - ) -> Request? { - self.retrieve(userID: userId, page: page).done { certificates, meta in - success(meta, certificates) - }.catch { error in - errorHandler(NetworkError(error: error)) - } - return nil + func update(_ certificate: Certificate) -> Promise { + self.update.request( + requestEndpoint: self.name, + paramName: "certificate", + updatingObject: certificate, + withManager: self.manager + ) } enum Order: String { diff --git a/Stepic/Legacy/Model/Parser.swift b/Stepic/Legacy/Model/Parser.swift index 7bf5ae5ed1..dfa1ff54f4 100644 --- a/Stepic/Legacy/Model/Parser.swift +++ b/Stepic/Legacy/Model/Parser.swift @@ -55,6 +55,13 @@ enum Parser { return UIColor(hex6: hexUIntValue) } + static func timedateStringFromDate(dateOrNil: Date?) -> String? { + if let date = dateOrNil { + return self.timedateStringFromDate(date: date) + } + return nil + } + static func timedateStringFromDate(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" diff --git a/Stepic/Sources/Helpers/FormatterHelper.swift b/Stepic/Sources/Helpers/FormatterHelper.swift index d9532778c4..2e8b6769dc 100644 --- a/Stepic/Sources/Helpers/FormatterHelper.swift +++ b/Stepic/Sources/Helpers/FormatterHelper.swift @@ -301,6 +301,19 @@ enum FormatterHelper { return "\(count) \(pluralizedCountString)" } + /// Format submissions count with localized and pluralized suffix; 1 -> "1 time", 5 -> "5 times" + static func timesCount(_ count: Int) -> String { + let pluralizedCountString = StringHelper.pluralize( + number: count, + forms: [ + NSLocalizedString("times1", comment: ""), + NSLocalizedString("times234", comment: ""), + NSLocalizedString("times567890", comment: "") + ] + ) + return "\(count) \(pluralizedCountString)" + } + // MARK: Date /// Format days count with localized and pluralized suffix; 1 -> "1 day", 5 -> "5 days" diff --git a/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesProvider.swift b/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesProvider.swift index caf5b7b4ed..ad16f42756 100644 --- a/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesProvider.swift +++ b/Stepic/Sources/Modules/NewProfileSubmodules/Certificates/NewProfileCertificatesProvider.swift @@ -54,16 +54,16 @@ final class NewProfileCertificatesProvider: NewProfileCertificatesProviderProtoc return } + let coursesMap = Dictionary(courses.map({ ($0.id, $0) }), uniquingKeysWith: { first, _ in first }) + for certificate in certificates { - if let course = courses.first(where: { $0.id == certificate.courseId }) { - certificate.course = course - } + certificate.course = coursesMap[certificate.courseID] } CoreDataHelper.shared.save() } - let courseIDs = certificates.map(\.courseId) + let courseIDs = certificates.map(\.courseID) return firstly { () -> Promise<([Course], Meta)> in self.coursesPersistenceService.fetch(ids: courseIDs) diff --git a/Stepic/Sources/Services/Models/Network/CertificatesNetworkService.swift b/Stepic/Sources/Services/Models/Network/CertificatesNetworkService.swift index 017feff2a9..a3b9a63a92 100644 --- a/Stepic/Sources/Services/Models/Network/CertificatesNetworkService.swift +++ b/Stepic/Sources/Services/Models/Network/CertificatesNetworkService.swift @@ -4,6 +4,8 @@ import PromiseKit protocol CertificatesNetworkServiceProtocol: AnyObject { func fetch(userID: User.IdType, page: Int) -> Promise<([Certificate], Meta)> func fetch(courseID: Course.IdType, userID: User.IdType) -> Promise<[Certificate]> + + func update(certificate: Certificate) -> Promise } extension CertificatesNetworkServiceProtocol { @@ -39,7 +41,18 @@ final class CertificatesNetworkService: CertificatesNetworkServiceProtocol { } } + func update(certificate: Certificate) -> Promise { + Promise { seal in + self.certificatesAPI.update(certificate).done { certificate in + seal.fulfill(certificate) + }.catch { _ in + seal.reject(Error.updateFailed) + } + } + } + enum Error: Swift.Error { case fetchFailed + case updateFailed } } diff --git a/Stepic/Sources/Services/Models/Persistence/CertificatesPersistenceService.swift b/Stepic/Sources/Services/Models/Persistence/CertificatesPersistenceService.swift index 30f1619bc5..8fcaf64fd0 100644 --- a/Stepic/Sources/Services/Models/Persistence/CertificatesPersistenceService.swift +++ b/Stepic/Sources/Services/Models/Persistence/CertificatesPersistenceService.swift @@ -4,7 +4,6 @@ import PromiseKit protocol CertificatesPersistenceServiceProtocol: AnyObject { func fetch(id: Certificate.IdType) -> Guarantee func fetch(ids: [Certificate.IdType]) -> Guarantee<[Certificate]> - func fetch(ids: [Certificate.IdType], userID: User.IdType) -> Guarantee<[Certificate]> func fetch(userID: User.IdType) -> Guarantee<[Certificate]> func fetch(courseID: Course.IdType, userID: User.IdType) -> Guarantee<[Certificate]> @@ -13,33 +12,6 @@ protocol CertificatesPersistenceServiceProtocol: AnyObject { final class CertificatesPersistenceService: BasePersistenceService, CertificatesPersistenceServiceProtocol { - func fetch(ids: [Certificate.IdType], userID: User.IdType) -> Guarantee<[Certificate]> { - Guarantee { seal in - do { - let certificates = try Certificate.fetch(in: self.managedObjectContext) { request in - let idPredicates = ids.map { id in - NSPredicate(format: "%K == %@", #keyPath(Certificate.managedId), NSNumber(value: id)) - } - let idCompoundPredicate = NSCompoundPredicate(type: .or, subpredicates: idPredicates) - let userPredicate = NSPredicate( - format: "%K == %@", - #keyPath(Certificate.managedUserId), - NSNumber(value: userID) - ) - request.predicate = NSCompoundPredicate( - type: .and, - subpredicates: [userPredicate, idCompoundPredicate] - ) - request.returnsObjectsAsFaults = false - } - seal(certificates) - } catch { - print("CertificatesPersistenceService :: failed fetch user certificates with error = \(error)") - seal([]) - } - } - } - func fetch(userID: User.IdType) -> Guarantee<[Certificate]> { Guarantee { seal in let request = Certificate.sortedFetchRequest @@ -62,7 +34,7 @@ final class CertificatesPersistenceService: BasePersistenceService, func fetch(courseID: Course.IdType, userID: User.IdType) -> Guarantee<[Certificate]> { Guarantee { seal in - let request: NSFetchRequest = Certificate.sortedFetchRequest + let request = Certificate.sortedFetchRequest request.returnsObjectsAsFaults = false let coursePredicate = NSPredicate( diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 567c70f281..67fda6f438 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -339,6 +339,9 @@ quizReviews567890 = "reviews"; submissions1 = "submission"; submissions234 = "submissions"; submissions567890 = "submissions"; +times1 = "time"; +times234 = "times"; +times567890 = "times"; SearchPlaceholderEmpty = "Sorry, we didn't find any courses matching your request"; SearchPlaceholderError = "Error while searching courses. Press to try again"; TagPlaceholderError = "Error while loading courses. Press to try again"; @@ -1335,6 +1338,17 @@ NewProfileUserActivityLongestStreak = "Max streak %@"; NewProfileStreakNotificationsNotifyPreference = "Notify About Streaks"; NewProfileStreakNotificationsTimeSelection = "Notification Time"; +/* Certificate name change */ +CertificateNameChangeAction = "Change Recipient Name"; +CertificateNameChangeAlertTitle = "Change Recipient Name"; +CertificateNameChangeAlertMessageWarning = "Warning! You can change the certificate recipient name only %@.\nEnter name:"; +CertificateNameChangeAlertTextFieldPlaceholder = "Recipient name"; +CertificateNameChangeAlertMessageConfirmation = "Are you sure that you want to change the certificate recipient name from \"%@\" to \"%@\"?"; +CertificateNameChangeAlertMessageLastEditWarning = "Warning! After this confirmation you will not be able to change the certificate recipient name."; +CertificateNameChangeSuccessStatusMessage = "Certificate recipient name changed"; +CertificateNameChangeErrorStatusMessage = "Failed to change certificate recipient name"; +CertificateNameChangeEmptyTextFieldErrorMessage = "Recipient name must not be empty"; + /* In-App Purchases */ IAPPurchaseSucceededTitle = "Purchase Succeeded"; IAPPurchaseFailedTitle = "Purchase Failed"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 0a248e7914..d30c86f3b1 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -340,6 +340,9 @@ quizReviews567890 = "рецензий"; submissions1 = "решение"; submissions234 = "решения"; submissions567890 = "решений"; +times1 = "раз"; +times234 = "раза"; +times567890 = "раз"; SearchPlaceholderEmpty = "Упс, мы не нашли ни одного курса по запросу"; SearchPlaceholderError = "Ошибка при поиске курсов. Нажмите, чтобы попробовать еще раз"; TagPlaceholderError = "Ошибка при загрузке курсов. Нажмите, чтобы попробовать еще раз"; @@ -1336,6 +1339,17 @@ NewProfileUserActivityLongestStreak = "Максимальная серия %@"; NewProfileStreakNotificationsNotifyPreference = "Уведомления о занятиях"; NewProfileStreakNotificationsTimeSelection = "Время напоминания"; +/* Certificate name change */ +CertificateNameChangeAction = "Изменить имя получателя"; +CertificateNameChangeAlertTitle = "Изменить имя получателя"; +CertificateNameChangeAlertMessageWarning = "Внимание! Изменить имя получателя сертификата можно только %@.\nВведите имя:"; +CertificateNameChangeAlertTextFieldPlaceholder = "Имя получателя"; +CertificateNameChangeAlertMessageConfirmation = "Вы уверены, что хотите изменить имя получателя сертификата с \"%@\" на \"%@\"?"; +CertificateNameChangeAlertMessageLastEditWarning = "Внимание! После этого изменения вы больше не сможете поменять имя."; +CertificateNameChangeSuccessStatusMessage = "Имя получателя сертификата изменено"; +CertificateNameChangeErrorStatusMessage = "Не удалось изменить имя получателя сертификата"; +CertificateNameChangeEmptyTextFieldErrorMessage = "Имя получателя не может быть пустым"; + /* In-App Purchases */ IAPPurchaseSucceededTitle = "Покупка удалась"; IAPPurchaseFailedTitle = "Покупка не удалась";