Skip to content

Commit

Permalink
Merge pull request #41 from tobyspark/feature-server-status
Browse files Browse the repository at this point in the history
Feature: Thing screen verified + published status updates from server

- gets study start+end dates from server
- gets video verified status from server
- displays video verified status, with messaging
- displays published status, based on study end date and verified status
  • Loading branch information
tobyspark authored May 24, 2020
2 parents 122bdb4 + 3b50d10 commit 821dc12
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 5 deletions.
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
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://example.com/phaseone/api/participant/"

static let endpointThing = "https://example.com/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()
}
}
41 changes: 41 additions & 0 deletions ORBIT Camera/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ struct Video: Codable, Equatable {
}
var kind: Kind

enum Verified: String, Codable, CustomStringConvertible {
case unvalidated = "-"
case rejectPII = "P"
case rejectInappropriate = "I"
case rejectMissingObject = "M"
case clean = "C"

var description: String {
switch self {
case .unvalidated: return "Not yet checked"
case .rejectPII: return "Can't use as reveals identity"
case .rejectInappropriate: return "Can't use as is inappropriate"
case .rejectMissingObject: return "Can't use as does not show object"
case .clean: return "Checked and is suitable"
}
}
}
var verified: Verified

init?(of thing: Thing, url: URL, kind: Kind) {
guard
let thingID = thing.id
Expand All @@ -72,6 +91,7 @@ struct Video: Codable, Equatable {
self.recorded = Date()
self.orbitID = nil
self.kind = kind
self.verified = .unvalidated

self.url = url
}
Expand All @@ -83,6 +103,26 @@ struct Video: Codable, Equatable {
private static var storageURL: URL {
try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!
}

// Private type property backing `url`
private static var storageCacheURL: URL {
try! FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) // FIXME: try!
}

private static var placeholderURL: URL {
Bundle.main.url(forResource: "orbit-cup-photoreal", withExtension: "mp4")! // FIXME: !
}

private func moveToCacheStorage() {
do {
try FileManager.default.moveItem(
at: URL(fileURLWithPath: filename, relativeTo: Video.storageURL),
to: URL(fileURLWithPath: filename, relativeTo: Video.storageCacheURL)
)
} catch {
print(error)
}
}
}

extension Video: FetchableRecord, MutablePersistableRecord {
Expand All @@ -93,6 +133,7 @@ extension Video: FetchableRecord, MutablePersistableRecord {
static let recorded = Column(CodingKeys.recorded)
static let orbitID = Column(CodingKeys.orbitID)
static let kind = Column(CodingKeys.kind)
static let verified = Column(CodingKeys.verified)
}

// Update auto-incremented id upon successful insertion
Expand Down

0 comments on commit 821dc12

Please sign in to comment.