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

Feature: Thing screen verified + published status updates from server #41

Merged
merged 7 commits into from
May 24, 2020
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
13 changes: 10 additions & 3 deletions ORBIT Camera Tests/PersistenceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ class PersistenceTests: XCTestCase {
}

// Mint participant
var participant = Settings.participant
try dbQueue.write { db in try participant.save(db) }
_ = try Participant.appParticipant()

// Mint five things, with five videos each
let testVideoURL = Bundle(for: type(of: self)).url(forResource: "orbit-cup-photoreal", withExtension:"mp4")!
Expand Down Expand Up @@ -84,9 +83,16 @@ class PersistenceTests: XCTestCase {
XCTAssertNotNil(participant.id, "Stored thing should have an ID")
try dbQueue.write { db in try participant.save(db) } // Extra save, should not insert new

let participants = try dbQueue.read { db in try Participant.fetchAll(db) }
var participants = try dbQueue.read { db in try Participant.fetchAll(db) }
XCTAssertEqual(participants.count, 1, "Persisting a participant should result in one thing persisted")
XCTAssertEqual(participant, participants[0], "Retreiving a persisted participant should return an identical participant")

participant.studyStart = Date()
participant.studyEnd = Date()
try dbQueue.write { db in try participant.save(db) }
participants = try dbQueue.read { db in try Participant.fetchAll(db) }
XCTAssertEqual(participant.studyStart!.description, participants[0].studyStart!.description, "Retreiving a persisted participant should return an identical participant")
XCTAssertEqual(participant.studyEnd!.description, participants[0].studyEnd!.description, "Retreiving a persisted participant should return an identical participant")
}

/// Persist a thing. Create it, write it to storage, read it from storage, check it's the same.
Expand Down Expand Up @@ -128,6 +134,7 @@ class PersistenceTests: XCTestCase {
XCTAssertEqual(video.recorded.description, videos[0].recorded.description, "Retreiving a persisted thing should return an identical thing") // Floating point internal representation is rounded to three decimal places on coding, so for expediency let's just compare the description.
XCTAssertEqual(video.orbitID, videos[0].orbitID, "Retreiving a persisted thing should return an identical thing")
XCTAssertEqual(video.kind, videos[0].kind, "Retreiving a persisted thing should return an identical thing")
XCTAssertEqual(video.verified, videos[0].verified, "Retreiving a persisted thing should return an identical thing")

video.orbitID = 456
try dbQueue.write { db in try video.save(db) }
Expand Down
8 changes: 8 additions & 0 deletions ORBIT Camera.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
4BD4A16924617FF800817FF4 /* ErrorLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A16824617FF800817FF4 /* ErrorLabel.swift */; };
4BE50EF52437F07C00BE79C7 /* Collection+SafeSubscript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE50EF42437F07C00BE79C7 /* Collection+SafeSubscript.swift */; };
4BE50EF72438879E00BE79C7 /* ThingCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE50EF62438879E00BE79C7 /* ThingCell.swift */; };
4BE77689247910470088562D /* Video+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE77688247910470088562D /* Video+ServerStatus.swift */; };
4BE7768C247A98360088562D /* Participant+ServerStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE7768B247A98350088562D /* Participant+ServerStatus.swift */; };
4BF14BBA2405730700E31D93 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BB92405730700E31D93 /* SceneDelegate.swift */; };
4BF14BBC2405730700E31D93 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBB2405730700E31D93 /* MasterViewController.swift */; };
4BF14BBE2405730700E31D93 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF14BBD2405730700E31D93 /* DetailViewController.swift */; };
Expand Down Expand Up @@ -114,6 +116,8 @@
4BD4A16824617FF800817FF4 /* ErrorLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLabel.swift; sourceTree = "<group>"; };
4BE50EF42437F07C00BE79C7 /* Collection+SafeSubscript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+SafeSubscript.swift"; sourceTree = "<group>"; };
4BE50EF62438879E00BE79C7 /* ThingCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThingCell.swift; sourceTree = "<group>"; };
4BE77688247910470088562D /* Video+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Video+ServerStatus.swift"; sourceTree = "<group>"; };
4BE7768B247A98350088562D /* Participant+ServerStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Participant+ServerStatus.swift"; sourceTree = "<group>"; };
4BF14BB42405730700E31D93 /* ORBIT Camera.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ORBIT Camera.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4BF14BB72405730700E31D93 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
4BF14BB92405730700E31D93 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -205,11 +209,13 @@
4BA13A59242D6EC4006D838B /* PreviewMetalView.swift */,
4BA13A5B242D71E4006D838B /* PassThrough.metal */,
4B87FD1024098D57007AB630 /* Participant.swift */,
4BE7768B247A98350088562D /* Participant+ServerStatus.swift */,
4B87FD32240EF49F007AB630 /* Uploadable.swift */,
4BF14BCF2405771900E31D93 /* Thing.swift */,
4B87FD0D2406A5DF007AB630 /* Thing+upload.swift */,
4B87FD1D240D4E93007AB630 /* Video.swift */,
4B87FD20240D53FC007AB630 /* Video+upload.swift */,
4BE77688247910470088562D /* Video+ServerStatus.swift */,
4B87FD2F240EC756007AB630 /* MultipartFormData.swift */,
4B8409B224499B470001F654 /* OrbitPagerView.swift */,
4B9AA9FE2418EA49002380B1 /* RecordButton.swift */,
Expand Down Expand Up @@ -364,6 +370,7 @@
buildActionMask = 2147483647;
files = (
4BFC5F252459DA6000E5A3C1 /* Settings+secrets.swift in Sources */,
4BE7768C247A98360088562D /* Participant+ServerStatus.swift in Sources */,
4BF14C192405B81900E31D93 /* AppDatabase.swift in Sources */,
4B87FD1124098D57007AB630 /* Participant.swift in Sources */,
4B3D772624332C740077DFD0 /* VideoKindPickerView.swift in Sources */,
Expand Down Expand Up @@ -393,6 +400,7 @@
4B87FD3924126110007AB630 /* VideoViewCell.swift in Sources */,
4BE50EF72438879E00BE79C7 /* ThingCell.swift in Sources */,
4B87FD3624111E57007AB630 /* AppDatabase+TestData.swift in Sources */,
4BE77689247910470088562D /* Video+ServerStatus.swift in Sources */,
4B5A6B992424CB9200A5C45B /* NewThingCell.swift in Sources */,
4B87FD21240D53FC007AB630 /* Video+upload.swift in Sources */,
4B3119B52451E99D00E28452 /* HelpViewController.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions ORBIT Camera/AppDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ struct AppDatabase {
}
}

migrator.registerMigration("addVerifiedToVideo") { db in
try db.alter(table: "video") { t in
t.add(column: "verified", .text)
}

try Video.updateAll(db, [Video.Columns.verified <- Video.Verified.unvalidated.rawValue])
}

migrator.registerMigration("addStudyDatesToParticipant") { db in
try db.alter(table: "participant") { t in
t.add(column: "studyStart", .blob)
t.add(column: "studyEnd", .blob)
}
}

return migrator
}
}
28 changes: 26 additions & 2 deletions ORBIT Camera/DetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,34 @@ class DetailViewController: UIViewController {
videoUploadedLabel.text = video.orbitID == nil ? "Not yet uploaded" : "Uploaded"
uploadedElement.accessibilityLabel = "ORBIT dataset status: \(videoUploadedLabel.text!)"

// TODO: videoVerified
switch video.verified {
case .unvalidated:
videoVerifiedIcon.image = UIImage(systemName: "checkmark.circle")
case .rejectInappropriate, .rejectMissingObject, .rejectPII:
videoVerifiedIcon.image = UIImage(systemName: "x.circle.fill")
case .clean:
videoVerifiedIcon.image = UIImage(systemName: "checkmark.circle.fill")
}
videoVerifiedLabel.text = video.verified.description
verifiedElement.accessibilityLabel = "\(videoVerifiedLabel.text!)"

// TODO: videoPublished
switch video.verified {
case .unvalidated:
videoPublishedIcon.image = UIImage(systemName: "lock.circle")
videoPublishedLabel.text = "Not yet published"
case .rejectInappropriate, .rejectMissingObject, .rejectPII:
videoPublishedIcon.image = UIImage(systemName: "lock.circle")
videoPublishedLabel.text = "Re-record to publish"
case .clean:
let published: Bool
if let studyEnd = try! Participant.appParticipant().studyEnd {
published = studyEnd < Date()
} else {
published = false
}
videoPublishedIcon.image = published ? UIImage(systemName: "lock.circle.fill") : UIImage(systemName: "lock.circle")
videoPublishedLabel.text = published ? "Published in dataset" : "Not yet published"
}
publishedElement.accessibilityLabel = "\(videoPublishedLabel.text!)"
}

Expand Down
66 changes: 66 additions & 0 deletions ORBIT Camera/Participant+ServerStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Participant+ServerStatus.swift
// ORBIT Camera
//
// Created by Toby Harris on 24/05/2020.
// Copyright © 2020 Toby Harris. All rights reserved.
//

import Foundation
import GRDB
import os

extension Participant {
struct APIGETResponse: Codable {
let studyStart: Date
let studyEnd: Date

enum CodingKeys: String, CodingKey {
case studyStart = "study_start"
case studyEnd = "study_end"
}
}

static func updateServerStatuses(url: URL = URL(string: Settings.endpointParticipant)!) {
var request = URLRequest(url: url)
request.setValue(appNetwork.authCredential, forHTTPHeaderField: "Authorization")

let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(Settings.apiDateFomatter)

let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
os_log("Participant.updateServerStatuses failed, received error", log: appNetLog)
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse
else {
os_log("Participant.updateServerStatuses failed, cannot parse response", log: appNetLog)
return
}
guard httpResponse.statusCode == 200
else {
os_log("Participant.updateServerStatuses failed: %d", log: appNetLog, httpResponse.statusCode)
return
}
guard
let mimeType = httpResponse.mimeType,
mimeType == "application/json",
let data = data,
let participantData = try? jsonDecoder.decode(APIGETResponse.self, from: data)
else {
os_log("Participant.updateServerStatuses failed, could not decode data")
return
}

var participant = try! Participant.appParticipant()
if participant.studyStart != participantData.studyStart || participant.studyEnd != participantData.studyEnd {
participant.studyStart = participantData.studyStart
participant.studyEnd = participantData.studyEnd
try! dbQueue.write { db in try participant.save(db) }
}
}
task.resume()
}
}
3 changes: 3 additions & 0 deletions ORBIT Camera/Participant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ struct Participant: Codable, Equatable {

/// Authorisation string for HTTP requests made for this participant. Should only be populated with validated credential.
var authCredential: String?

var studyStart: Date?
var studyEnd: Date?
}

extension Participant: FetchableRecord, MutablePersistableRecord {
Expand Down
7 changes: 7 additions & 0 deletions ORBIT Camera/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UISplitViewControllerDe
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.

// Update video statuses from server
// Delay allows authCredential be set on app launch
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(1)) {
Participant.updateServerStatuses()
Video.updateServerStatuses()
}
}

func sceneDidEnterBackground(_ scene: UIScene) {
Expand Down
8 changes: 8 additions & 0 deletions ORBIT Camera/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct Settings {
struct endpointCreateParticipantResponse: Codable {
let auth_credential: String
}

static let endpointParticipant = "https://orbit-data.city.ac.uk/phaseone/api/participant/"

static let endpointThing = "https://orbit-data.city.ac.uk/phaseone/api/thing/"
static func endpointThing(id orbitID: Int) -> URL {
Expand Down Expand Up @@ -50,4 +52,10 @@ struct Settings {
df.timeStyle = .short
return df
}()

static let apiDateFomatter: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "yyyy-MM-dd"
return df
}()
}
86 changes: 86 additions & 0 deletions ORBIT Camera/Video+ServerStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//
// Video+ServerStatus.swift
// ORBIT Camera
//
// Created by Toby Harris on 23/05/2020.
// Copyright © 2020 Toby Harris. All rights reserved.
//

import Foundation
import GRDB
import os

extension Video {
struct APIGETItemResponse: Codable {
let id: Int
let thing: Int
let file: String
let technique: String
let validation: String
}

struct APIGETPageResponse: Codable {
let count: Int
let next: String?
let previous: String?
let results: [APIGETItemResponse]
}

static func updateServerStatuses(url: URL = URL(string: Settings.endpointVideo)!) {
var request = URLRequest(url: url)
request.setValue(appNetwork.authCredential, forHTTPHeaderField: "Authorization")

let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
os_log("Video.updateServerStatuses failed, received error", log: appNetLog)
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse
else {
os_log("Video.updateServerStatuses failed, cannot parse response", log: appNetLog)
return
}
guard httpResponse.statusCode == 200
else {
os_log("Video.updateServerStatuses failed: %d", log: appNetLog, httpResponse.statusCode)
return
}
guard
let mimeType = httpResponse.mimeType,
mimeType == "application/json",
let data = data,
let pageData = try? JSONDecoder().decode(APIGETPageResponse.self, from: data)
else {
os_log("Video.updateServerStatuses failed, could not decode data")
return
}

// Update videos
try! dbQueue.write { db in
for result in pageData.results {
guard var video = try Video.filter(Video.Columns.orbitID == result.id).fetchOne(db)
else {
os_log("Video GET returned unknown orbitID: %d", log: appNetLog, type: .error, result.id)
continue
}
guard let verifiedServerStatus = Video.Verified.init(rawValue: result.validation)
else {
os_log("Video GET returned unknown validation: %{public}s", log: appNetLog, type: .error, result.validation)
continue
}
if video.verified != verifiedServerStatus {
video.verified = verifiedServerStatus
try video.save(db)
}
}
}

// Process next page if needed
if let nextPage = pageData.next {
Video.updateServerStatuses(url: URL(string: nextPage)!)
}
}
task.resume()
}
}
Loading