Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adaptive reactions for exam application #348

Merged
merged 63 commits into from
Aug 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
cd19d40
Create ServiceFactoryMock and needed components
ivan-magda Aug 16, 2018
4ea222e
Test ServiceFactory
ivan-magda Aug 16, 2018
2352449
Create AssemblyFactoryMock and needed componenets
ivan-magda Aug 16, 2018
30ae867
Rearrange source code structure
ivan-magda Aug 16, 2018
7e2d78b
Factory methods naming instead of creator
ivan-magda Aug 16, 2018
e47007f
Test AssemblyFactory
ivan-magda Aug 16, 2018
003d28d
Test UITableView specific extensions
ivan-magda Aug 16, 2018
9da1681
Rearrange source code structure
ivan-magda Aug 16, 2018
18e8eed
Update RouterDismissable implemetation
ivan-magda Aug 16, 2018
c99bd8a
Test BaseRouter
ivan-magda Aug 16, 2018
4efa7b9
Fix operator function whitespace violation
ivan-magda Aug 16, 2018
30a257f
Create RecommendationsService
ivan-magda Aug 17, 2018
9e59a26
Refactor BaseServiceMock to PromiseReturnable
ivan-magda Aug 17, 2018
21429a1
Extend RecommendationsService with ability to fetch LessonPlainObjects
ivan-magda Aug 17, 2018
3dc72db
Create ReactionService
ivan-magda Aug 17, 2018
13f2a40
Display segmented control
ivan-magda Aug 17, 2018
8b9cf75
Show blank adaptive screen
ivan-magda Aug 17, 2018
53b820c
Attempt to cache knowledge graph but failure
ivan-magda Aug 17, 2018
90e24bd
Revert "Attempt to cache knowledge graph but failure"
ivan-magda Aug 17, 2018
e851f5d
Add dummy knowledge graph's cache support
ivan-magda Aug 18, 2018
5639368
Create FileStorage
ivan-magda Aug 20, 2018
14df0a9
Update GraphService
ivan-magda Aug 20, 2018
4105728
Cache knowledge graph data
ivan-magda Aug 20, 2018
a65af7f
Move KnowledgeGraphProvider to ServiceFactory
ivan-magda Aug 20, 2018
8b049bf
Show empty adaptive module
ivan-magda Aug 20, 2018
e59a40f
Inject AdaptiveStepsPresenter with service dependencies
ivan-magda Aug 20, 2018
b7410b1
Set connections for steps with lessons if needed
ivan-magda Aug 20, 2018
fcb29e4
Fetch and show adaptive steps
ivan-magda Aug 20, 2018
fb9acac
Fix scroll view lagging bug
ivan-magda Aug 20, 2018
7bcdf1d
Create JoinCourseUseCase and UseCaseFactory
ivan-magda Aug 20, 2018
b097226
Set need new attempt for adaptive steps
ivan-magda Aug 20, 2018
6ded2fc
Create SendStepViewUseCase
ivan-magda Aug 20, 2018
6061109
Implement send reaction method
ivan-magda Aug 20, 2018
f959a16
Simplify some logic
ivan-magda Aug 21, 2018
adca067
Create StepModuleSeed
ivan-magda Aug 21, 2018
8d750da
Show progress hud
ivan-magda Aug 21, 2018
50a2219
Register placeholders
ivan-magda Aug 21, 2018
bbe0cb5
Restructure code
ivan-magda Aug 21, 2018
af95cb4
Refactor move AdaptiveSteps module to the Steps group
ivan-magda Aug 21, 2018
61e4c4d
Create compound StepsAssembly
ivan-magda Aug 21, 2018
1720433
Fix tests
ivan-magda Aug 21, 2018
3e4d898
Replace unnecessary PromiseReturnable protocol with BaseServiceMock
ivan-magda Aug 21, 2018
d611cc9
Minor updates
ivan-magda Aug 21, 2018
73528cd
Remove managed objects mappers
ivan-magda Aug 23, 2018
baa4003
Use PromiseKit in FileStorage
ivan-magda Aug 23, 2018
f8d5330
Replace use case usages with services
ivan-magda Aug 23, 2018
4b6c90f
Show toolbar
ivan-magda Aug 23, 2018
5d91494
Hide submit button for adaptive steps
ivan-magda Aug 23, 2018
34aa458
Keep track of step progress
ivan-magda Aug 23, 2018
b57fc20
Store toolbar items as lazy variable
ivan-magda Aug 23, 2018
6149e3d
Update submit button title
ivan-magda Aug 23, 2018
2fb07b1
Show and hide toolbar items based on view state
ivan-magda Aug 23, 2018
ba114ff
Extend AdaptiveStepsPresenterProtocol
ivan-magda Aug 23, 2018
c42d83c
Fix with new StepPlainObject API
ivan-magda Aug 23, 2018
7e78c3c
Extend StepPresenterDelegate
ivan-magda Aug 23, 2018
0d94e31
Extend StepPresenter with submit and retry ability
ivan-magda Aug 23, 2018
c136693
Add submit and reactions functionality for adaptive presenter
ivan-magda Aug 23, 2018
6da481c
Fix tests
ivan-magda Aug 23, 2018
9a1ced8
Merge remote-tracking branch 'origin/dev' into feature/exam-adaptive-…
ivan-magda Aug 24, 2018
4460dd3
Minor fixes
ivan-magda Aug 24, 2018
354e0b7
Merge remote-tracking branch 'origin/dev' into feature/exam-adaptive-…
ivan-magda Aug 24, 2018
02a9c4a
Localize
ivan-magda Aug 24, 2018
6abf6a9
Merge branch 'dev' into feature/exam-adaptive-reactions
ivan-magda Aug 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ExamEGERussian/Sources/CoreLayer/Graph/Base/Vertex.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension Vertex: Hashable {
return "\(id)".hashValue
}

static public func == (lhs: Vertex, rhs: Vertex) -> Bool {
public static func == (lhs: Vertex, rhs: Vertex) -> Bool {
return lhs.id == rhs.id
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class QuizViewControllerBuilder {
private(set) weak var logoutable: Logoutable?
private(set) var stepType: StepPlainObject.StepType?
private(set) var needNewAttempt = false
private(set) var isSubmitButtonHidden = false

func setStepType(_ stepType: StepPlainObject.StepType) -> QuizViewControllerBuilder {
self.stepType = stepType
Expand All @@ -28,6 +29,11 @@ final class QuizViewControllerBuilder {
return self
}

func setSubmitButtonHidden(_ isSubmitButtonHidden: Bool) -> QuizViewControllerBuilder {
self.isSubmitButtonHidden = isSubmitButtonHidden
return self
}

func build() -> QuizViewController? {
guard let stepType = stepType else {
return nil
Expand Down
39 changes: 38 additions & 1 deletion ExamEGERussian/Sources/Model/PlainObjects/StepPlainObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ struct StepPlainObject {
let text: String
let type: StepType
let progressId: String?
var isPassed = false
var state: Progress = .default

var isPassed: Bool {
return state == .successful
}

var image: UIImage {
switch type {
Expand All @@ -31,6 +35,10 @@ struct StepPlainObject {
}
}

mutating func setPassed(_ passed: Bool) {
state = passed ? .successful : .unsolved
}

// MARK: Types

enum StepType: String {
Expand All @@ -50,4 +58,33 @@ struct StepPlainObject {
case dataset
case admin
}

enum Progress {
case successful
case unsolved
case wrong

static var `default`: Progress {
return .unsolved
}
}
}

extension StepPlainObject {
init(id: Int,
lessonId: Int,
position: Int,
text: String,
type: StepType,
progressId: String?,
isPassed: Bool = false
) {
self.id = id
self.lessonId = lessonId
self.position = position
self.text = text
self.type = type
self.progressId = progressId
setPassed(isPassed)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ final class AdaptiveStepsPresenter: AdaptiveStepsPresenterProtocol {
private let courseService: CourseService
private let viewsService: ViewsServiceProtocol

private var stepViewController: UIViewController?
private var currentStepViewController: UIViewController?
private var currentStepPresenter: StepPresenter?
private var currentStep: StepPlainObject?
private var currentStepProgress: StepPlainObject.Progress {
return currentStep?.state ?? .default
}

private var cachedRecommendedLessons = [LessonPlainObject]()
private static let recommendationsBatchSize = 6
Expand All @@ -47,17 +51,73 @@ final class AdaptiveStepsPresenter: AdaptiveStepsPresenterProtocol {
self.viewsService = viewsService
}

// MARK: - Public API

func refresh() {
currentStep = nil

updateView()
startRecommendationsPipeline()
}

private func sendReaction(_ reaction: Reaction) -> Promise<Void> {
func submit() {
view?.state = .fetching

switch currentStepProgress {
case .unsolved:
currentStepPresenter?.submit()
case .wrong:
currentStepPresenter?.retry()
case .successful:
showNextTask()
}
}

func sendHardReaction() {
sendReaction(.maybeLater)
}

func sendEasyReaction() {
sendReaction(.neverAgain)
}

// MARK: - Private API

private func updateView() {
view?.state = .idle

switch currentStepProgress {
case .unsolved:
view?.updateSubmitButtonTitle(NSLocalizedString("AdaptiveControlButtonSubmit", comment: ""))
case .wrong:
view?.updateSubmitButtonTitle(NSLocalizedString("AdaptiveControlButtonTryAgain", comment: ""))
case .successful:
view?.updateSubmitButtonTitle(NSLocalizedString("AdaptiveControlButtonNextTask", comment: ""))
}
}

private func showNextTask() {
refresh()
}

private func sendReaction(_ reaction: Reaction) {
guard let lessonId = currentStep?.lessonId,
let user = AuthInfo.shared.user else {
return Promise(error: AdaptiveStepsError.reactionNotSent)
return
}

return reactionService.sendReaction(reaction, forLesson: lessonId, byUser: user.id)
view?.state = .fetching

reactionService.sendReaction(reaction, forLesson: lessonId, byUser: user.id).done {
if reaction == .solved {
self.view?.state = .idle
} else {
self.showNextTask()
}
}.catch { error in
print("\(#function): error: \(error)")
self.view?.state = .connectionError
}
}

// MARK: - Types
Expand Down Expand Up @@ -184,19 +244,28 @@ extension AdaptiveStepsPresenter {

extension AdaptiveStepsPresenter {
private func showStepViewController(for step: StepPlainObject, lesson: LessonPlainObject) {
let newStepViewController = buildStepViewController(for: step, lesson: lesson)
if let stepViewController = stepViewController {
view?.removeContentController(stepViewController)
if self.currentStepViewController != nil {
view?.removeContentController(self.currentStepViewController!)
}

stepViewController = newStepViewController
view?.addContentController(newStepViewController)
let stepViewController = buildStepViewController(for: step, lesson: lesson)
self.currentStepViewController = stepViewController
self.currentStepPresenter = stepViewController.presenter

view?.addContentController(stepViewController)
view?.updateTitle(lesson.title)
}

private func buildStepViewController(for step: StepPlainObject, lesson: LessonPlainObject) -> UIViewController {
let builder = QuizViewControllerBuilder().setNeedNewAttempt(true)
let seed = StepModuleSeed(lesson: lesson, step: step, quizViewControllerBuilder: builder, stepPresenterDelegate: self)
private func buildStepViewController(for step: StepPlainObject, lesson: LessonPlainObject) -> StepViewController {
let builder = QuizViewControllerBuilder()
.setNeedNewAttempt(true)
.setSubmitButtonHidden(true)
let seed = StepModuleSeed(
lesson: lesson,
step: step,
quizViewControllerBuilder: builder,
stepPresenterDelegate: self
)

return stepAssembly.module(seed: seed)
}
Expand All @@ -206,12 +275,18 @@ extension AdaptiveStepsPresenter {

extension AdaptiveStepsPresenter: StepPresenterDelegate {
func stepPresenterSubmissionDidCorrect(_ stepPresenter: StepPresenter) {
currentStep?.isPassed = true
currentStep?.state = .successful
updateView()
sendReaction(.solved)
}

sendReaction(.solved).done {
self.refresh()
}.catch { error in
print("\(#function): error: \(error)")
}
func stepPresenterSubmissionDidWrong(_ stepPresenter: StepPresenter) {
currentStep?.state = .wrong
updateView()
}

func stepPresenterSubmissionDidRetry(_ stepPresenter: StepPresenter) {
currentStep?.state = .unsolved
updateView()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ import Foundation

protocol AdaptiveStepsPresenterProtocol: class {
func refresh()
func submit()
func sendHardReaction()
func sendEasyReaction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ protocol AdaptiveStepsView: class {
func removeContentController(_ controller: UIViewController)

func updateTitle(_ title: String)
func updateSubmitButtonTitle(_ title: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ final class AdaptiveStepsViewController: UIViewController, ControllerWithStepikP
case .idle:
SVProgressHUD.dismiss()
isPlaceholderShown = false
setToolbarItemsEnabled(true)
case .fetching:
SVProgressHUD.show()
isPlaceholderShown = false
setToolbarItemsEnabled(false)
case .coursePassed:
SVProgressHUD.dismiss()
showPlaceholder(for: .adaptiveCoursePassed)
setToolbarItemsEnabled(false)
case .connectionError:
SVProgressHUD.dismiss()
showPlaceholder(for: .connectionError)
setToolbarItemsEnabled(false)
}
}
}
Expand All @@ -36,21 +40,73 @@ final class AdaptiveStepsViewController: UIViewController, ControllerWithStepikP

private weak var stepView: UIView?

private lazy var hardBarButtonItem: UIBarButtonItem = {
UIBarButtonItem(title: NSLocalizedString("AdaptiveHardReaction", comment: ""), style: .plain,
target: self, action: #selector(onHardClick(_:)))
}()
private lazy var easyBarButtonItem: UIBarButtonItem = {
UIBarButtonItem(title: NSLocalizedString("AdaptiveEasyReaction", comment: ""), style: .plain,
target: self, action: #selector(onEasyClick(_:)))
}()

private lazy var submitBarButtonItem: UIBarButtonItem = {
UIBarButtonItem(title: NSLocalizedString("Submit", comment: ""), style: .plain,
target: self, action: #selector(onSubmitClick(_:)))
}()

override func viewDidLoad() {
super.viewDidLoad()

setup()
presenter?.refresh()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setToolbarHidden(false, animated: animated)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setToolbarHidden(true, animated: animated)
}

// MARK: - Private API

private func setup() {
view.backgroundColor = .white

let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
target: self, action: nil)
toolbarItems = [hardBarButtonItem, spacer, submitBarButtonItem, spacer,
easyBarButtonItem]

registerPlaceholder(placeholder: StepikPlaceholder(.noConnectionQuiz, action: { [weak self] in
self?.presenter?.refresh()
}), for: .connectionError)
registerPlaceholder(placeholder: StepikPlaceholder(.adaptiveCoursePassed), for: .adaptiveCoursePassed)
}

private func setToolbarItemsEnabled(_ enabled: Bool) {
toolbarItems?.forEach {
$0.isEnabled = enabled
}
}

@objc
private func onHardClick(_ sender: Any) {
presenter?.sendHardReaction()
}

@objc
private func onSubmitClick(_ sender: Any) {
presenter?.submit()
}

@objc
private func onEasyClick(_ sender: Any) {
presenter?.sendEasyReaction()
}
}

// MARK: - AdaptiveStepViewController: AdaptiveStepView -
Expand All @@ -77,4 +133,8 @@ extension AdaptiveStepsViewController: AdaptiveStepsView {
func updateTitle(_ title: String) {
self.title = title
}

func updateSubmitButtonTitle(_ title: String) {
submitBarButtonItem.title = title
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ extension StepsPagerPresenterImpl: StepPresenterDelegate {
// MARK: Private Helpers

private func updateStepProgress(at index: Int, passed: Bool) {
steps[index].isPassed = passed
steps[index].setPassed(passed)
view?.setTabSelected(passed, at: index)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Foundation

protocol StepAssembly: class {
func module(seed: StepModuleSeed) -> UIViewController
func module(seed: StepModuleSeed) -> StepViewController
}

class StepModuleSeed {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import Foundation

final class StepAssemblyImpl: BaseAssembly, StepAssembly {
func module(seed: StepModuleSeed) -> UIViewController {
func module(seed: StepModuleSeed) -> StepViewController {
let controller = StepViewController()
let router = StepRouter(viewController: controller, authAssembly: assemblyFactory.authAssembly)

Expand Down
Loading