@@ -79,7 +86,7 @@
{{/if}}
- {{#if (or contest.disallowTabSwitch contest.disallowWindowResize)}}
+ {{#if (or contest.disallowTabSwitch contest.disallowWindowResize contest.disallowNoFace)}}
{{#if contest.disallowTabSwitch}}
Tab Switching is prohibited on this contest. You will face a penalty of 10 mins in case you :-
@@ -96,6 +103,12 @@
• Resize the browser window
{{/if}}
+ {{#if contest.disallowWindowResize}}
+
Face detection is enabled on this contest. You will face a penalty of 10 mins every 5 secs in case :-
+
+ - • Your face is not visible in camera.
+
+ {{/if}}
{{/if}}
diff --git a/app/pods/contests/contest/attempt/controller.js b/app/pods/contests/contest/attempt/controller.js
index 210989e..5021834 100644
--- a/app/pods/contests/contest/attempt/controller.js
+++ b/app/pods/contests/contest/attempt/controller.js
@@ -6,6 +6,42 @@ import { later } from '@ember/runloop';
export default class AttemptController extends Controller{
@service api
+ @service monitorer
+ @service router
+
+ isMonitorerSet = false
+
+ init() {
+ this._super(...arguments)
+
+ this.setupMonitorer = this.setupMonitorer.bind(this)
+ }
+
+ async setupMonitorer() {
+ if(this.isMonitorerSet) return
+
+ await this.monitorer.setup({
+ contest: this.contest,
+ onError: this.onMonitorerError.bind(this)
+ })
+
+ this.set('isMonitorerSet', true)
+ }
+
+ async onMonitorerError(detail) {
+ await this.monitorer.disable()
+ this.set('isMonitorerSet', false)
+
+ switch(detail.code) {
+ case "CAMERAACCESSDENIED":
+ this.transitionToRoute('contests.contest', this.contest.id, {
+ queryParams: {
+ monitorerError: detail.code
+ }
+ })
+ break;
+ }
+ }
@dropTask submitTask = function* () {
later(() => {
diff --git a/app/pods/contests/contest/attempt/route.js b/app/pods/contests/contest/attempt/route.js
index e159178..0bbe510 100644
--- a/app/pods/contests/contest/attempt/route.js
+++ b/app/pods/contests/contest/attempt/route.js
@@ -5,6 +5,7 @@ import { action } from '@ember/object';
export default class AttemptRoute extends Route {
@service navigation;
@service currentUser;
+ @service monitorer
async beforeModel() {
super.beforeModel()
@@ -61,4 +62,12 @@ export default class AttemptRoute extends Route {
}
throw err
}
+ @action
+ async willTransition(transition) {
+ this._super(...arguments)
+ if(!transition.to.name.includes('contests.contest.attempt.content')) {
+ this.controller.set('isMonitorerSet', false)
+ await this.monitorer.disable()
+ }
+ }
}
diff --git a/app/pods/contests/contest/attempt/template.hbs b/app/pods/contests/contest/attempt/template.hbs
index 835b47b..7083c42 100644
--- a/app/pods/contests/contest/attempt/template.hbs
+++ b/app/pods/contests/contest/attempt/template.hbs
@@ -3,6 +3,7 @@
@attempt={{contest.currentAttempt}}
@onTimerEnd={{onTimerEnd}}
@submitTask={{submitTask}}
- @contest={{contest}}>
+ @contest={{contest}}
+ @setupMonitorer={{setupMonitorer}}>
{{outlet}}
\ No newline at end of file
diff --git a/app/pods/contests/contest/index/controller.js b/app/pods/contests/contest/index/controller.js
index f3bd1b0..6346dde 100644
--- a/app/pods/contests/contest/index/controller.js
+++ b/app/pods/contests/contest/index/controller.js
@@ -7,12 +7,13 @@ export default class IndexController extends Controller {
@service store
@service router
- queryParams = ['offset', 'limit', 'status', 'difficulty', 'tags', 'q']
+ queryParams = ['offset', 'limit', 'status', 'difficulty', 'tags', 'q', 'monitorerError']
offset = 0
limit = 10
difficulty = []
tags = []
q = ''
+ monitorerError = null
@computed('offset', 'limit')
get page() {
diff --git a/app/pods/contests/contest/index/route.js b/app/pods/contests/contest/index/route.js
index 83a9960..a1c75ca 100644
--- a/app/pods/contests/contest/index/route.js
+++ b/app/pods/contests/contest/index/route.js
@@ -22,6 +22,9 @@ export default class IndexRoute extends Route {
},
q: {
refreshModel: true
+ },
+ monitorerError: {
+ refreshModel: false
}
}
diff --git a/app/pods/contests/contest/index/template.hbs b/app/pods/contests/contest/index/template.hbs
index 2b75dc8..1e12aa2 100644
--- a/app/pods/contests/contest/index/template.hbs
+++ b/app/pods/contests/contest/index/template.hbs
@@ -11,7 +11,8 @@
@nextRoute={{nextRoute}}
@handleUnverifiedEmail={{handleUnverifiedEmail}}
@onAfterCreate={{onAfterCreate}}
- @contest={{contest}} />
+ @contest={{contest}}
+ @monitorerError={{monitorerError}} />
{{else}}
diff --git a/app/services/monitorer.js b/app/services/monitorer.js
index c6d6881..0efe951 100644
--- a/app/services/monitorer.js
+++ b/app/services/monitorer.js
@@ -1,122 +1,119 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
-import { timeout } from 'ember-concurrency';
-import { later } from '@ember/runloop';
+// import Monitorer from '@coding-blocks/monitorer';
export default Service.extend({
router: service(),
api: service(),
store: service(),
- isTabSwitchEventListenerAdded: false,
tabSwitchTrigger: false,
+ noFaceTrigger: false,
windowResizeTrigger: false,
- isWindowResizeEventThrottled: false,
- isBrowserFullScreened: window.screen.availHeight === window.outerHeight && window.screen.availWidth === window.outerWidth,
- monitoredRoutes: [
- 'contests.contest.attempt.content.problem',
- 'contests.contest.attempt.content.quiz',
- // 'contests.contest.attempt.content.project',
- ],
- windowResizeInterval: null,
- isCurrentRouteMonitored: computed('router.currentRouteName', function(){
- return this.monitoredRoutes.includes(this.router.currentRouteName)
- }),
- isTabSwitchDisabledOnContest: computed('router.currentRoute.attributes.contest', function() {
- if(this.router.currentRoute) {
- return !!this.router.get('currentRoute.attributes.contest.disallowTabSwitch')
- } else return false
- }),
- isWindowResizeDisabledOnContest: computed('router.currentRoute.attributes.contest', function() {
- if(this.router.currentRoute) {
- return !!this.router.get('currentRoute.attributes.contest.disallowWindowResize')
- } else return false
- }),
- isMonitoringEnabled: computed('isCurrentRouteMonitored', 'isTabSwitchDisabledOnContest', 'isWindowResizeDisabledOnContest', function() {
- return this.isCurrentRouteMonitored && (this.isTabSwitchDisabledOnContest || this.isWindowResizeDisabledOnContest)
- }),
+ noFaceThrottled: false,
+ noFaceDetected: false,
+ isMonitorerFaultEventHandlerAdded: false,
+ failureRedirect: null,
init() {
this._super(...arguments)
- this.tabSwitchEventHandler = this.tabSwitchEventHandler.bind(this)
- this.windowResizeEventHandler = this.windowResizeEventHandler.bind(this)
- this.windowResizeFaultReporter = this.windowResizeFaultReporter.bind(this)
- this.addObserver('isMonitoringEnabled', this, 'enableOrDisableMonitorerEvents')
- this.enableOrDisableMonitorerEvents()
+ this.monitorerFaultEventHandler = this.monitorerFaultEventHandler.bind(this)
+ this.monitorerErrorEventHandler = this.monitorerErrorEventHandler.bind(this)
+ this.monitorerSuccessEventHandler = this.monitorerSuccessEventHandler.bind(this)
},
- setIsBrowserFullScreened() {
- this.set('isBrowserFullScreened', window.screen.availHeight <= window.outerHeight && window.screen.availWidth <= window.outerWidth)
- },
+ async setup(options) {
+ if(!this.monitorer) {
+ this.set('monitorer', new Monitorer())
+ }
- async enableOrDisableMonitorerEvents() {
- console.log('enable monitorer')
- this.setIsBrowserFullScreened()
-
- if(!this.isMonitoringEnabled) {
- if(this.isTabSwitchEventListenerAdded) {
- document.removeEventListener('visibilitychange', this.tabSwitchEventHandler)
- this.set('isTabSwitchEventListenerAdded', false)
- }
+ this.set('contest', options.contest)
+ this.set('onError', options.onError)
- if(this.isWindowResizeEventListenerAdded) {
- window.removeEventListener('resize', this.windowResizeEventHandler)
- return this.set('isWindowResizeEventListenerAdded', false)
- }
- if(this.windowResizeInterval) {
- clearInterval(this.windowResizeInterval)
- this.set('windowResizeInterval', null)
- }
+ if(!this.isMonitorerFaultEventHandlerAdded) {
+ window.addEventListener('monitorerfault', this.monitorerFaultEventHandler)
+ window.addEventListener('monitorererror', this.monitorerErrorEventHandler)
+ window.addEventListener('monitorersuccess', this.monitorerSuccessEventHandler)
}
-
- if(this.isTabSwitchDisabledOnContest && !this.isTabSwitchEventListenerAdded) {
- this.setTabSwitchEvents()
- this.set('isTabSwitchEventListenerAdded', true)
+
+
+ if(this.contest.disallowTabSwitch) {
+ await this.enableTabSwitchMonitorer()
}
-
- if(this.isWindowResizeDisabledOnContest && !this.isWindowResizeEventListenerAdded) {
- this.setWindowResizeEvents()
- this.set('isWindowResizeEventListenerAdded', true)
- later(() => {
- })
+
+ if(this.contest.disallowWindowResize) {
+ await this.enableWindowResizeMonitorer()
+ }
+
+ if(this.contest.disallowNoFace) {
+ await this.enableNoFaceMonitorer({ noFace: true })
}
},
- clearPreviousEventListeners() {
- getEventListeners(document)
+ async disable() {
+ this.set('contest', null)
+ this.set('onError', null)
+
+ await this.disableTabSwitchMonitorer()
+ await this.disableWindowResizeMonitorer()
+ await this.disableNoFaceMonitorer()
+
+ window.removeEventListener('monitorerfault', this.monitorerFaultEventHandler)
+ },
+
+ async enableTabSwitchMonitorer() {
+ await this.monitorer.enable({ tabSwitch: true })
},
- async setTabSwitchEvents() {//called based on route activation
- const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt')
+ async enableWindowResizeMonitorer() {
+ await this.monitorer.enable({ windowResize: true })
+ },
+
+ async enableNoFaceMonitorer() {
+ await this.monitorer.enable({ noFace: true })
+ await this.monitorer.enable({ liveFeed: true })
+ },
+
+ async disableTabSwitchMonitorer() {
+ await this.monitorer.disable({ tabSwitch: true })
+ },
+
+ async disableWindowResizeMonitorer() {
+ await this.monitorer.disable({ windowResize: true })
+ },
+
+ async disableNoFaceMonitorer() {
+ await this.monitorer.disable({ noFace: true })
+ await this.monitorer.disable({ liveFeed: true })
+ },
+
+ async monitorerFaultEventHandler(e) {
+ const currentAttempt = await this.contest.currentAttempt
if(!!!currentAttempt.id) return
- if('webkitHidden' in document) {
- document.addEventListener("webkitvisibilitychange", this.tabSwitchEventHandler);
- console.log('webkitvisibilitychange event added')
- } else {
- document.addEventListener("visibilitychange", this.tabSwitchEventHandler);
- console.log('visibilitychange event added')
+ switch(e.detail.code) {
+ case "TAB_SWITCHED": await this.handleTabSwitchFault(); break;
+ case "WINDOW_RESIZED": await this.handleWindowResizeFault(e.detail); break;
+ case "NO_FACE_DETECTED": await this.handleNoFaceFault(e.detail);
+ this.set('noFaceDetected', true); break;
}
},
-
- async setWindowResizeEvents() {//called based on route activation
- const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt')
- if(!!!currentAttempt.id) return
- if(!this.isBrowserFullScreened) {
- if(!this.windowResizeInterval) {
- this.windowResizeFaultReporter(true)
- this.set('windowResizeInterval', setInterval(this.windowResizeFaultReporter, 10000))
- }
+ async monitorerErrorEventHandler(e) {
+ if(this.onError) {
+ this.onError(e.detail)
}
- window.addEventListener("resize", this.windowResizeEventHandler);
-
},
- async tabSwitchEventHandler() {
- console.log('tabSwitchEventHandler', document.hidden, document.webkitHidden)
+ async monitorerSuccessEventHandler(e) {
+ switch(e.detail.code) {
+ case "ONEFACEDETECTED": this.set('noFaceDetected', false); break;
+ }
+ },
+
+ async handleTabSwitchFault() {
if(!document.hidden) return this.set('tabSwitchTrigger', true)
- const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt')
+ const currentAttempt = await this.contest.currentAttempt
await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, {
method: 'POST',
data: {
@@ -126,30 +123,11 @@ export default Service.extend({
await this.store.findRecord('contest-attempt', currentAttempt.id)
},
- async windowResizeEventHandler() {
- if(!this.isWindowResizeEventThrottled) {
- this.set('isWindowResizeEventThrottled', true)
- if(!this.windowResizeInterval) {
- this.windowResizeFaultReporter(true)
- this.set('windowResizeInterval', setInterval(this.windowResizeFaultReporter, 10000))
- }
- //to prevent multiple event hits if user is continuously resizing
- setTimeout(() => this.set('isWindowResizeEventThrottled', false), 2500)
- }
- },
-
- async windowResizeFaultReporter(delayTrigger = false) {
- this.setIsBrowserFullScreened()
- if(this.isBrowserFullScreened) {
- this.set('isWindowResizeEventThrottled', false)
- if(this.windowResizeInterval) {
- clearInterval(this.windowResizeInterval)
- this.set('windowResizeInterval', null)
- }
- }
- else {
+ async handleWindowResizeFault(details) {
+ if(details.message === 'browser_unfullscreened') {
this.set('windowResizeTrigger', true)
- const currentAttempt = await this.router.get('currentRoute.attributes.contest.currentAttempt')
+
+ const currentAttempt = await this.contest.currentAttempt
await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, {
method: 'POST',
data: {
@@ -159,4 +137,25 @@ export default Service.extend({
await this.store.findRecord('contest-attempt', currentAttempt.id)
}
},
+
+ async handleNoFaceFault(details) {
+ if(!this.noFaceThrottled) {
+ this.set('noFaceThrottled', true)
+ setTimeout(() => this.set('noFaceThrottled', false), 5000)
+
+ this.set('noFaceTrigger', true)
+
+ const currentAttempt = await this.contest.currentAttempt
+ const imgFileArray = new Uint8Array(await details.imageBlob.arrayBuffer())
+
+ await this.api.request(`/contest-attempts/${currentAttempt.id}/report-monitorer-fault`, {
+ method: 'POST',
+ data: {
+ fault_type: 'no_face',
+ image_file_array: imgFileArray
+ }
+ })
+ await this.store.findRecord('contest-attempt', currentAttempt.id)
+ }
+ }
});
diff --git a/app/styles/app.scss b/app/styles/app.scss
index 7d08876..d212504 100644
--- a/app/styles/app.scss
+++ b/app/styles/app.scss
@@ -70,10 +70,19 @@ body{
width: 250%;
}
+.top-left {
+ top: 0;
+ left: 0;
+}
+
.border-dark-pink {
border: solid 2px #f24f5f;
}
+.border-green {
+ border: solid 2px #2cc528;
+}
+
.red {
color: #f24f5f !important;
}