Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def all_pods
pod 'lottie-ios'
pod 'Koloda', '4.3.1'
pod 'SDWebImage/GIF'
pod 'Charts', '3.0.2'
end

def testing_pods
Expand Down
446 changes: 282 additions & 164 deletions Stepic.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

467 changes: 449 additions & 18 deletions Stepic/Adaptive.storyboard

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions Stepic/AdaptiveRatingHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// AdaptiveRatingHelper.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 03.08.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation

class AdaptiveRatingHelper {
static func getLevel(for rating: Int) -> Int {
return rating < 5 ? 1 : 2 + Int(log(Double(rating) / 5.0) / log(2.0))
}

static func getRating(for level: Int) -> Int {
return level == 0 ? 0 : (level == 1 ? 5 : 5 * Int(pow(2.0, Double(level - 1))))
}
}
51 changes: 51 additions & 0 deletions Stepic/AdaptiveRatingManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// AdaptiveRatingManager.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 12.07.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation

class AdaptiveRatingManager {

let courseId: Int

private lazy var ratingKey: String = {
"rating_\(self.courseId)"
}()

private lazy var streakKey: String = {
"streak_\(self.courseId)"
}()

let defaults = UserDefaults.standard

init(courseId: Int) {
self.courseId = courseId
}

var rating: Int {
get {
return defaults.integer(forKey: ratingKey)
}
set(newValue) {
updateValue(newValue, for: ratingKey)
}
}

var streak: Int {
get {
return max(1, defaults.integer(forKey: streakKey))
}
set(newValue) {
updateValue(newValue, for: streakKey)
}
}

private func updateValue(_ newValue: Int, for key: String) {
defaults.set(newValue, forKey: key)
defaults.synchronize()
}
}
80 changes: 80 additions & 0 deletions Stepic/AdaptiveRatingsAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// AdaptiveRatingsAPI.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 15.08.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation
import Alamofire
import SwiftyJSON
import PromiseKit

class AdaptiveRatingsAPI: APIEndpoint {
override var name: String { return "rating" }

typealias RatingRecord = (userId: Int, exp: Int, rank: Int)
typealias Scoreboard = (allCount: Int, leaders: [RatingRecord])

func update(courseId: Int, exp: Int) -> Promise<Void> {
var params: Parameters = [
"course": courseId,
"exp": exp
]

if let token = AuthInfo.shared.token?.accessToken {
params["token"] = token
}

return Promise { fulfill, reject in
manager.request("\(StepicApplicationsInfo.adaptiveRatingURL)/\(name)", method: .put, parameters: params, encoding: JSONEncoding.default, headers: nil).responseSwiftyJSON { response in
switch response.result {
case .failure(let error):
reject(error)
case .success(_):
switch response.response?.statusCode ?? 500 {
case 200: fulfill()
case 401: reject(RatingsAPIError.badRequest)
default: reject(RatingsAPIError.serverError)
}
}
}
}
}

func retrieve(courseId: Int, count: Int = 10, days: Int? = 7) -> Promise<Scoreboard> {
var params: Parameters = [
"course": courseId,
"count": count
]

if let days = days {
params["days"] = days
}

if let userId = AuthInfo.shared.userId {
params["user"] = userId
}

return Promise { fulfill, reject in
manager.request("\(StepicApplicationsInfo.adaptiveRatingURL)/\(name)", method: .get, parameters: params, encoding: URLEncoding.default, headers: nil).responseSwiftyJSON { response in
switch response.result {
case .failure(let error):
reject(error)
case .success(let json):
if response.response?.statusCode == 200 {
let leaders = json["users"].arrayValue.map { RatingRecord(userId: $0["user"].intValue, exp: $0["exp"].intValue, rank: $0["rank"].intValue) }
fulfill(Scoreboard(allCount: json["count"].intValue, leaders: leaders))
} else {
reject(RatingsAPIError.serverError)
}
}
}
}
}
}

enum RatingsAPIError: Error {
case badRequest, serverError, connectionError(error: String)
}
129 changes: 129 additions & 0 deletions Stepic/AdaptiveRatingsPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// AdaptiveRatingsPresenter.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 15.08.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation
import Alamofire
import PromiseKit

protocol AdaptiveRatingsView: class {
func reload()
func setRatings(data: ScoreboardViewData)
func showError()
}

struct RatingViewData {
let position: Int
let exp: Int
let name: String
let me: Bool
}

struct ScoreboardViewData {
let allCount: Int
let leaders: [RatingViewData]
}

class AdaptiveRatingsPresenter {
weak var view: AdaptiveRatingsView?

fileprivate var ratingsAPI: AdaptiveRatingsAPI
fileprivate var ratingManager: AdaptiveRatingManager

private var scoreboard: [Int: ScoreboardViewData] = [:]

// Names (word + grammatical gender)
private var nouns: [(String, String)] = []
private var adjs: [(String, String)] = []

init(ratingsAPI: AdaptiveRatingsAPI, ratingManager: AdaptiveRatingManager, view: AdaptiveRatingsView) {
self.view = view
self.ratingManager = ratingManager
self.ratingsAPI = ratingsAPI

loadNamesFromFiles()
}

func reloadData(days: Int? = nil, force: Bool = false) {
// Send rating first, then get rating
ratingsAPI.cancelAllTasks()
ratingsAPI.update(courseId: ratingManager.courseId, exp: ratingManager.rating).then { _ -> Promise<ScoreboardViewData> in
print("adaptive ratings: remote rating updated -> reload rating")
let downloadedScoreboard = self.scoreboard[days ?? 0] // 0 when 'days' == nil
if downloadedScoreboard == nil || force {
return self.reloadRating(days: days, force: force)
} else {
return Promise(value: downloadedScoreboard!)
}
}.then { scoreboard -> Void in
self.scoreboard[days ?? 0] = scoreboard
self.view?.setRatings(data: scoreboard)
self.view?.reload()
}.catch { error in
switch error {
case RatingsAPIError.serverError:
print("adaptive ratings: remote rating update failed: server error")
AnalyticsReporter.reportEvent(AnalyticsEvents.Errors.adaptiveRatingServer)
default:
print("adaptive ratings: remote rating update failed: \(error)")
}
self.view?.showError()
}
}

fileprivate func reloadRating(days: Int? = nil, force: Bool = false) -> Promise<ScoreboardViewData> {
return Promise { fulfill, reject in
let currentUser = AuthInfo.shared.userId

ratingsAPI.cancelAllTasks()
ratingsAPI.retrieve(courseId: ratingManager.courseId, count: 10, days: days).then { scoreboard -> Void in
var curLeaders: [RatingViewData] = []
scoreboard.leaders.forEach { record in
curLeaders.append(RatingViewData(position: record.rank, exp: record.exp, name: self.generateNameBy(userId: record.userId), me: currentUser == record.userId))
}

let curScoreboard = ScoreboardViewData(allCount: scoreboard.allCount, leaders: curLeaders)
fulfill(curScoreboard)
}.catch { error in
reject(error)
}
}
}

fileprivate func loadNamesFromFiles() {
func readFile(name: String) -> [String] {
if let path = Bundle.main.path(forResource: name, ofType: "plist"),
let words = NSArray(contentsOfFile: path) as? [String] {
return words
}
return []
}

readFile(name: "adjectives_m").forEach { adjs.append(($0, "m")) }
readFile(name: "adjectives_f").forEach { adjs.append(($0, "f")) }
readFile(name: "nouns_m").forEach { nouns.append(($0, "m")) }
readFile(name: "nouns_f").forEach { nouns.append(($0, "f")) }

assert(adjs.count % 2 == 0)
}

fileprivate func generateNameBy(userId: Int) -> String {
func hash(_ id: Int) -> Int {
var x = id
x = ((x >> 16) ^ x) &* 0x45d9f3b
x = ((x >> 16) ^ x) &* 0x45d9f3b
x = (x >> 16) ^ x
return x % (nouns.count * (adjs.count / 2))
}

let noun = nouns[hash(userId) % nouns.count]
let adjsByGender = adjs.flatMap { noun.1 == $0.1 ? $0 : nil }
let adjNum = hash(userId) / nouns.count

return "\(adjsByGender[adjNum].0.capitalized) \(noun.0)"
}
}
Loading