Skip to content

Commit

Permalink
Adaptive scoreboard (#92)
Browse files Browse the repository at this point in the history
* Add proxy-submissions methods

* Add raiting API methods

* UI for leaderboard

* Refactor AdaptiveStatsPresenter

* Add ranks

* Create card with leaderboard

* Add fake generated names

* Medal assets

* Add rating separator

* Add current request handling

* Fix separator bug

* Add rating table footer

* Fix footer label

* Add migration

* Add L10n

* Fix shadow bug

* Add pluralization

* txt -> plist

* Add adaptive SubmissionsAPI implementation

* Fix weeks order

* Rating update in stepSubmissionDidCorrect()

* Change func signatures

* New rating backend

* Add rating backend URL

* Fix colors
  • Loading branch information
kvld authored Aug 23, 2017
1 parent a90ff2b commit 767bd7c
Show file tree
Hide file tree
Showing 43 changed files with 1,592 additions and 182 deletions.
190 changes: 190 additions & 0 deletions Stepic.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Stepic/ApplicationInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ApplicationInfo {
static let isAdaptive = "adaptive.isAdaptive"
static let courseId = "adaptive.courseId"
static let mainColor = "adaptive.mainColor"
static let ratingURL = "adaptive.ratingURL"
}
struct RateApp {
static let submissionsThreshold = "rateApp.submissionsThreshold"
Expand Down
4 changes: 2 additions & 2 deletions Stepic/QuizViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,8 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource {
guard
let encodedUrl = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encodedUrl)
else {
return
else {
return
}

WebControllerManager.sharedManager.presentWebControllerWithURL(url, inController: self, withKey: "external link", allowsSafari: true, backButtonStyle: BackButtonStyle.close)
Expand Down
1 change: 1 addition & 0 deletions Stepic/StepicApplicationsInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct StepicApplicationsInfo {
static let isAdaptive = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.isAdaptive) as? Bool ?? false
static let adaptiveCourseId = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.courseId) as? Int ?? 0
static let adaptiveMainColor = UIColor(hex: StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.mainColor) as? Int ?? 6736998)
static let adaptiveRatingURL = StepicApplicationsInfo.stepikConfigDic?.get(for: Root.Adaptive.ratingURL) as? String ?? ""

// Section: RateApp
struct RateApp {
Expand Down
5 changes: 5 additions & 0 deletions Stepic/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ RetentionNotificationYesterdayZero = "You didn't score any points for yesterday.
RetentionNotificationYesterdayStreak = "You have been learning %@ in a row. Go back and keep learning!";
RetentionNotificationWeekly = "You haven't done it for a long time. Go back and keep learning!";
SocialSignupWithExistingEmail = "This email is already used. To log in using this social account you have to log in via email and password";
AdaptiveRatingFooterText1 = "%@ user in the table";
AdaptiveRatingFooterText234 = "%@ users in the table";
AdaptiveRatingFooterText567890 = "%@ users in the table";
AdaptiveRatingYou = "You";
AdaptiveRatingLoadError = "Rating is unavailable now";
AdaptiveAchievementFirstStep = "First steps";
AdaptiveAchievementFirstStepDesc = "Complete tutorial";
AdaptiveAchievementShare = "Sociable";
Expand Down
5 changes: 5 additions & 0 deletions Stepic/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ RetentionNotificationYesterdayZero = "За вчерашний день Вы ни
RetentionNotificationYesterdayStreak = "Вы занимаетесь уже %@ подряд. Возвращайтесь и продолжайте учиться!";
RetentionNotificationWeekly = "Вы давно не занимались. Возвращайтесь и продолжайте учиться!";
SocialSignupWithExistingEmail = "Указанный email привязан к другому аккаунту. Чтобы входить через эту социальную сеть, необходимо войти, используя email и пароль";
AdaptiveRatingFooterText1 = "%@ пользователь в рейтинге";
AdaptiveRatingFooterText234 = "%@ пользователя в рейтинге";
AdaptiveRatingFooterText567890 = "%@ пользователей в рейтинге";
AdaptiveRatingYou = "Вы";
AdaptiveRatingLoadError = "Рейтинг недоступен";
AdaptiveAchievementFirstStep = "Первые шаги";
AdaptiveAchievementFirstStepDesc = "Пройти обучение";
AdaptiveAchievementShare = "Общительный";
Expand Down
51 changes: 51 additions & 0 deletions StepicAdaptiveCourse/AdaptiveAchievementsPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// AdaptiveAchievementsPresenter.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 15.08.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation

protocol AdaptiveAchievementsView: class {
func reload()
func setAchievements(records: [AchievementViewData])
}

struct AchievementViewData {
let name: String
let info: String
let type: AchievementType
let cover: UIImage?
let isUnlocked: Bool
let currentProgress: Int
let maxProgress: Int
}

class AdaptiveAchievementsPresenter {
weak var view: AdaptiveAchievementsView?

fileprivate var achievementsManager: AchievementManager

private var achievements: [AchievementViewData]?

init(achievementsManager: AchievementManager, view: AdaptiveAchievementsView) {
self.view = view

self.achievementsManager = achievementsManager
}

func reloadData(force: Bool = false) {
if achievements == nil || force {
achievements = []
achievementsManager.storedAchievements.forEach({ achievement in
achievements!.append(AchievementViewData(name: achievement.name, info: achievement.info ?? "", type: achievement.type, cover: achievement.cover, isUnlocked: achievement.isUnlocked, currentProgress: achievement.progressValue, maxProgress: achievement.maxProgressValue))
})
}

view?.setAchievements(records: achievements!)

view?.reload()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "medal3@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "medal2@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "medal1@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "more@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions StepicAdaptiveCourse/AdaptiveRatingsPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//
// AdaptiveRatingsPresenter.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 15.08.2017.
// Copyright © 2017 Alex Karpov. All rights reserved.
//

import Foundation
import Alamofire

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: RatingsAPI
fileprivate var ratingManager: RatingManager

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

private var currentRequest: Request?

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

init(ratingsAPI: RatingsAPI, ratingManager: RatingManager, 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
currentRequest?.cancel()
currentRequest = ratingsAPI.update(courseId: StepicApplicationsInfo.adaptiveCourseId, exp: ratingManager.rating, success: { _ in
print("remote rating updated -> reload rating")
self.reloadRating(days: days, force: force)
}, error: { responseStatus in
switch responseStatus {
case .serverError:
print("remote rating update failed: server error")
AnalyticsReporter.reportEvent(AnalyticsEvents.Adaptive.ratingServerError)
case .connectionError(let error):
print("remote rating update failed: \(error)")
default:
print("remote rating update failed: \(responseStatus)")
}
self.view?.setRatings(data: ScoreboardViewData(allCount: 0, leaders: []))
self.view?.reload()
self.view?.showError()
})
}

fileprivate func reloadRating(days: Int? = nil, force: Bool = false) {
let downloadedScoreboard = scoreboard[days ?? 0] // 0 when 'days' == nil
if downloadedScoreboard == nil || force {
let currentUser = AuthInfo.shared.userId

currentRequest?.cancel()
currentRequest = ratingsAPI.retrieve(courseId: StepicApplicationsInfo.adaptiveCourseId, count: 10, days: days, success: { scoreboard 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)
self.scoreboard[days ?? 0] = curScoreboard
self.view?.setRatings(data: curScoreboard)
self.view?.reload()
}, error: { err in
print(err)
self.view?.setRatings(data: ScoreboardViewData(allCount: 0, leaders: []))
self.view?.reload()
self.view?.showError()
})
} else {
view?.setRatings(data: downloadedScoreboard!)
view?.reload()
}
}

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

0 comments on commit 767bd7c

Please sign in to comment.