Skip to content

Commit

Permalink
Adaptive mode for exam application (#346)
Browse files Browse the repository at this point in the history
* Create RecommendationsService

* Create ReactionService

* Display segmented control

* Create FileStorage

* Cache knowledge graph data

* Set connections for steps with lessons if needed

* Fetch and show adaptive steps

* Fix scroll view lagging bug

* Set need new attempt for adaptive steps

* Implement send reaction method

* Create StepModuleSeed

* Restructure code

* Create compound StepsAssembly
  • Loading branch information
ivan-magda authored Aug 24, 2018
1 parent af08776 commit 57bfa5c
Show file tree
Hide file tree
Showing 130 changed files with 2,914 additions and 441 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ protocol ServiceFactory: class {
var enrollmentsAPI: EnrollmentsAPI { get }
var lessonsAPI: LessonsAPI { get }
var progressesAPI: ProgressesAPI { get }
var recommendationsAPI: RecommendationsAPI { get }
var unitsAPI: UnitsAPI { get }
var viewsAPI: ViewsAPI { get }
var defaultsStorageManager: DefaultsStorageManager { get }

var userRegistrationService: UserRegistrationService { get }
var graphService: GraphService { get }
var graphService: GraphServiceProtocol { get }
var lessonsService: LessonsService { get }
var courseService: CourseService { get }
var enrollmentService: EnrollmentService { get }
var stepsService: StepsService { get }
var progressService: ProgressService { get }
var recommendationsService: RecommendationsServiceProtocol { get }
var reactionService: ReactionServiceProtocol { get }
var viewsService: ViewsServiceProtocol { get }

var knowledgeGraphProvider: KnowledgeGraphProviderProtocol { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import Foundation

final class ServiceFactoryImpl: ServiceFactory {

// MARK: - ServiceFactory -
private static let knowledgeGraphDirectoryName = "knowledge-graph"

let authAPI: AuthAPI
let stepicsAPI: StepicsAPI
Expand All @@ -20,6 +19,9 @@ final class ServiceFactoryImpl: ServiceFactory {
let lessonsAPI: LessonsAPI
let stepsAPI: StepsAPI
let progressesAPI: ProgressesAPI
let recommendationsAPI: RecommendationsAPI
let unitsAPI: UnitsAPI
let viewsAPI: ViewsAPI

let defaultsStorageManager: DefaultsStorageManager

Expand All @@ -32,30 +34,51 @@ final class ServiceFactoryImpl: ServiceFactory {
)
}

var graphService: GraphService {
return GraphServiceImpl()
var graphService: GraphServiceProtocol {
let fileStorage = FileStorage(destination: .atFolder(name: ServiceFactoryImpl.knowledgeGraphDirectoryName))
return GraphService(fileStorage: fileStorage)
}

var lessonsService: LessonsService {
return LessonsServiceImpl(lessonsAPI: lessonsAPI)
}

var courseService: CourseService {
return CourseServiceImpl(coursesAPI: coursesAPI, progressesService: self.progressService)
return CourseServiceImpl(
coursesAPI: coursesAPI,
progressesService: progressService,
enrollmentService: enrollmentService
)
}

var enrollmentService: EnrollmentService {
return EnrollmentServiceImpl(enrollmentsAPI: enrollmentsAPI)
}

var stepsService: StepsService {
return StepsServiceImpl(stepsAPI: stepsAPI, progressService: self.progressService)
return StepsServiceImpl(stepsAPI: stepsAPI, progressService: progressService)
}

var progressService: ProgressService {
return ProgressServiceImpl(progressesAPI: progressesAPI)
}

var recommendationsService: RecommendationsServiceProtocol {
return RecommendationsService(recommendationsAPI: recommendationsAPI, lessonsService: lessonsService)
}

var reactionService: ReactionServiceProtocol {
return ReactionService(recommendationsAPI: recommendationsAPI)
}

var viewsService: ViewsServiceProtocol {
return ViewsService(unitsAPI: unitsAPI, viewsAPI: viewsAPI)
}

var knowledgeGraphProvider: KnowledgeGraphProviderProtocol {
return CachedKnowledgeGraphProvider(graphService: graphService)
}

// MARK: - Init -

init(authAPI: AuthAPI,
Expand All @@ -66,6 +89,9 @@ final class ServiceFactoryImpl: ServiceFactory {
lessonsAPI: LessonsAPI,
stepsAPI: StepsAPI,
progressesAPI: ProgressesAPI,
recommendationsAPI: RecommendationsAPI,
unitsAPI: UnitsAPI,
viewsAPI: ViewsAPI,
defaultsStorageManager: DefaultsStorageManager
) {
self.authAPI = authAPI
Expand All @@ -76,6 +102,9 @@ final class ServiceFactoryImpl: ServiceFactory {
self.lessonsAPI = lessonsAPI
self.stepsAPI = stepsAPI
self.progressesAPI = progressesAPI
self.recommendationsAPI = recommendationsAPI
self.unitsAPI = unitsAPI
self.viewsAPI = viewsAPI
self.defaultsStorageManager = defaultsStorageManager
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ import Foundation
import PromiseKit

protocol CourseService: class {
/// Method is used to fetch Course objects from Stepik API
/// Method is used to fetch courses from Stepik API.
///
/// - Parameter ids: Ids Course objects
/// - Returns: Promise with a result of an array of Course objects from API.
func fetchCourses(with ids: [Int]) -> Promise<[Course]>
/// Method is used to obtain Course objects from cache with ids
/// - Parameter ids: Courses ids.
/// - Returns: `Promise<[CoursePlainObject]]>` if the fetch was successfully sent. \
/// Returns `error` if an error occurred.
func fetchCourses(with ids: [Int]) -> Promise<[CoursePlainObject]>
/// Method is used to obtain Course objects from cache with ids.
///
/// - Parameter ids: Ids Course objects
/// - Returns: Promise with a result of an array of Course objects from cache.
func obtainCourses(with ids: [Int]) -> Promise<[Course]>
/// - Parameter ids: Courses ids.
/// - Returns: `Promise<[CoursePlainObject]]>` if the courses was successfully \
/// obtained from the cache. Returns `error` if an error occurred.
func obtainCourses(with ids: [Int]) -> Promise<[CoursePlainObject]>
/// Method is used to joining courses by their ids.
///
/// - Parameter ids: Courses ids.
/// - Returns: An array of joined courses with element of type `CoursePlainObject`.
func joinCourses(with ids: [Int]) -> Promise<[CoursePlainObject]>
/// Method is used to fetch progresses for `Course` objects from Stepik API.
///
/// - Parameter ids: `Course` object ids.
/// - Parameter ids: Courses ids.
/// - Returns: Promise with an array of `Course` objects, that contains referenced `Progress` object.
func fetchProgresses(coursesIds ids: [Int]) -> Promise<[Course]>
func fetchProgresses(coursesIds ids: [Int]) -> Promise<[CoursePlainObject]>
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,48 @@ import PromiseKit
final class CourseServiceImpl: CourseService {
private let coursesAPI: CoursesAPI
private let progressService: ProgressService
private let enrollmentService: EnrollmentService

init(coursesAPI: CoursesAPI, progressesService: ProgressService) {
init(coursesAPI: CoursesAPI,
progressesService: ProgressService,
enrollmentService: EnrollmentService
) {
self.coursesAPI = coursesAPI
self.progressService = progressesService
self.enrollmentService = enrollmentService
}

func fetchCourses(with ids: [Int]) -> Promise<[Course]> {
guard !ids.isEmpty else {
return .value([])
}
// MARK: - Public API

return obtainCourses(with: ids).then { courses in
self.coursesAPI.retrieve(ids: ids, existing: courses)
func fetchCourses(with ids: [Int]) -> Promise<[CoursePlainObject]> {
return fetchManagedCourses(with: ids).mapValues {
CoursePlainObject(course: $0)
}
}

func obtainCourses(with ids: [Int]) -> Promise<[Course]> {
func obtainCourses(with ids: [Int]) -> Promise<[CoursePlainObject]> {
guard !ids.isEmpty else {
return .value([])
}

return Course.fetchAsync(ids)
return Course.fetchAsync(ids).mapValues { CoursePlainObject(course: $0) }
}

func fetchProgresses(coursesIds ids: [Int]) -> Promise<[Course]> {
func fetchProgresses(coursesIds ids: [Int]) -> Promise<[CoursePlainObject]> {
guard !ids.isEmpty else {
return .value([])
}

var courses = [Course]()

return obtainCourses(with: ids).then { cachedCourses -> Promise<[Progress]> in
return Course.fetchAsync(ids).then { cachedCourses -> Promise<[Progress]> in
courses = cachedCourses
let progressesIds = courses.compactMap {
$0.progressId
}

return self.progressService.fetchProgresses(with: progressesIds)
}.then { progresses -> Promise<[Course]> in
}.then { progresses -> Promise<[CoursePlainObject]> in
progresses.forEach { progress in
guard let course = courses.filter({ $0.progressId == progress.id }).first else {
return
Expand All @@ -59,7 +62,49 @@ final class CourseServiceImpl: CourseService {
}
CoreDataHelper.instance.save()

return .value(courses)
return .value(courses.map { CoursePlainObject(course: $0) })
}
}

func joinCourses(with ids: [Int]) -> Promise<[CoursePlainObject]> {
guard !ids.isEmpty else {
return .value([])
}

return obtainCourses(with: ids).then { courses -> Promise<[Int]> in
var ids = Set(ids)
courses
.filter { $0.enrolled }
.map { $0.id }
.forEach { ids.remove($0) }

return .value(Array(ids))
}.then { ids -> Promise<[Course]> in
self.fetchManagedCourses(with: ids)
}.then { courses in
when(fulfilled: courses.map { self.joinCourse($0) })
}.then { courses in
self.fetchProgresses(coursesIds: courses.map { $0.id })
}
}

// MARK: - Private API

private func fetchManagedCourses(with ids: [Int]) -> Promise<[Course]> {
guard !ids.isEmpty else {
return .value([])
}

return Course.fetchAsync(ids).then { courses in
self.coursesAPI.retrieve(ids: ids, existing: courses)
}
}

private func joinCourse(_ course: Course) -> Promise<Course> {
guard !course.enrolled else {
return .value(course)
}

return enrollmentService.joinCourse(course)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,61 @@

import Foundation
import PromiseKit
import Alamofire

protocol GraphService: class {
/// Method is used to fetch KnowledgeGraphPlainObject object from API.
///
/// - Returns: Promise with a result of KnowledgeGraphPlainObject.
func fetchGraph() -> Promise<KnowledgeGraphPlainObject>
final class GraphService: GraphServiceProtocol {
private static let url = URL(string: "https://www.dropbox.com/s/l8n1wny8qu0gbqt/example.json?dl=1")!
private static let fileName = "plain-object-response"

private let fileStorage: FileStorage

init(fileStorage: FileStorage) {
self.fileStorage = fileStorage
}

func fetchGraph() -> Promise<KnowledgeGraphPlainObject> {
return Alamofire
.request(GraphService.url)
.responseData()
.then { self.persistData($0.data) }
.then { self.decodeData($0) }
}

func obtainGraph() -> Promise<KnowledgeGraphPlainObject> {
return obtainFromCache()
}

private func obtainFromCache() -> Promise<KnowledgeGraphPlainObject> {
if let data = fileStorage.loadData(fileName: GraphService.fileName) {
return decodeData(data)
}

return Promise(error: GraphServiceError.noDataAtPath)
}

private func persistData(_ data: Data) -> Guarantee<Data> {
return Guarantee { seal in
self.fileStorage.persist(data: data, named: GraphService.fileName).done { _ in
seal(data)
}.catch { error in
print("\(#function): unable to persist data with error: \(error)")
seal(data)
}
}
}

private func decodeData(_ data: Data) -> Promise<KnowledgeGraphPlainObject> {
do {
let knowledgeGraph = try JSONDecoder().decode(KnowledgeGraphPlainObject.self, from: data)
return .value(knowledgeGraph)
} catch let error {
return Promise(error: GraphServiceError.unableToDecode(message: error.localizedDescription))
}
}

enum GraphServiceError: Error {
case noDataAtPath
case unableToPersist(message: String)
case unableToDecode(message: String)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// GraphServiceProtocol.swift
// ExamEGERussian
//
// Created by Ivan Magda on 18/07/2018.
// Copyright © 2018 Alex Karpov. All rights reserved.
//

import Foundation
import PromiseKit

protocol GraphServiceProtocol: class {
/// Method is used to fetch `KnowledgeGraphPlainObject` object from API.
///
/// - Returns: Promise with a result of `KnowledgeGraphPlainObject`.
func fetchGraph() -> Promise<KnowledgeGraphPlainObject>
/// Method is used to obtain `KnowledgeGraphPlainObject` object from cache.
///
/// - Returns: Promise with `KnowledgeGraphPlainObject` from cache.
func obtainGraph() -> Promise<KnowledgeGraphPlainObject>
}
Loading

0 comments on commit 57bfa5c

Please sign in to comment.