diff --git a/hackathon_site/event/admin.py b/hackathon_site/event/admin.py index eadbf4b8c..58a7f703e 100644 --- a/hackathon_site/event/admin.py +++ b/hackathon_site/event/admin.py @@ -4,7 +4,7 @@ from import_export import resources from import_export.admin import ExportMixin -from event.models import Profile, Team as EventTeam, User +from event.models import Profile, Team as EventTeam, User, UserActivity from hardware.admin import OrderInline admin.site.unregister(User) @@ -96,5 +96,28 @@ def get_members_count(self, obj): return obj.members_count +@admin.register(UserActivity) +class UserActivityAdmin(ExportMixin, admin.ModelAdmin): + list_display = ( + "get_user_name", + "sign_in", + "lunch1", + "dinner1", + "breakfast2", + "lunch2", + ) + + def get_user_name(self, obj): + return f"{obj.user.first_name} {obj.user.last_name}" + + get_user_name.short_description = "Name" + + def get_queryset(self, request): + return super().get_queryset(request).select_related("user") + + def get_export_queryset(self, request): + return super().get_queryset(request).select_related("user") + + # Register your models here. admin.site.register(Profile) diff --git a/hackathon_site/event/jinja2/event/admin_qr_scanner.html b/hackathon_site/event/jinja2/event/admin_qr_scanner.html new file mode 100644 index 000000000..cbf754044 --- /dev/null +++ b/hackathon_site/event/jinja2/event/admin_qr_scanner.html @@ -0,0 +1,127 @@ +{% extends "event/base.html" %} + +{% block nav_links %} +
Event | +Time | +Sign In Interval | +
---|---|---|
{{ event.description }} | +{{ event.time.strftime("%H:%M, %b %d") }} | +{{ get_sign_in_interval(event.time) }} | +
Clicking any of the below buttons will export the respective data to google sheets in this folder. Files will not be replaced, a new file will be created any time new data is exported.
+(Work In Progress)
+Total number of sign-ups: | +345 | +
Total number of completed applications: | +215 | +
Total number of completed reviews: | +0 | +
Make sure you read the participant package for all the info regarding the event, and join our {{ chat_room_name }}. Stay tuned for more updates regarding detailed event logistics, and we hope to see you soon!
Please show the QR code below to the front desk to sign-in. .
+ +If you have questions, read the FAQ, or feel free to contact us.
a?2:a>c?c:a;h=n-3;h=2>b?2:b>h?h:b;k=0;for(m=-2;2>=m;m++)for(p=-2;2>=p;p++)k+=u.get(c+m,h+p);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)m=8*a+h,p=8*b+k,t=l.get(m,p),q.set(m,p,t<=c),f&&r.set(m,p,!(t<=c))}f=f?{binarized:q,inverted:r}:{binarized:q};let {binarized:z,inverted:y}=f;(f=V(d? +y:z))||"attemptBoth"!==e.inversionAttempts&&"invertFirst"!==e.inversionAttempts||(f=V(d?z:y));return f}X.default=X;let Y="dontInvert",Z={red:77,green:150,blue:29,useIntegerApproximation:!0}; +self.onmessage=a=>{let b=a.data.id,c=a.data.data;switch(a.data.type){case "decode":(a=X(c.data,c.width,c.height,{inversionAttempts:Y,greyScaleWeights:Z}))?self.postMessage({id:b,type:"qrResult",data:a.data,cornerPoints:[a.location.topLeftCorner,a.location.topRightCorner,a.location.bottomRightCorner,a.location.bottomLeftCorner]}):self.postMessage({id:b,type:"qrResult",data:null});break;case "grayscaleWeights":Z.red=c.red;Z.green=c.green;Z.blue=c.blue;Z.useIntegerApproximation=c.useIntegerApproximation; +break;case "inversionMode":switch(c){case "original":Y="dontInvert";break;case "invert":Y="onlyInvert";break;case "both":Y="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}} +`, + ]), + { type: "application/javascript" } + ) + ); //# sourceMappingURL=qr-scanner-worker.min.js.map diff --git a/hackathon_site/event/static/event/js/qr-scanner.umd.min.js b/hackathon_site/event/static/event/js/qr-scanner.umd.min.js new file mode 100644 index 000000000..0bcdd4a46 --- /dev/null +++ b/hackathon_site/event/static/event/js/qr-scanner.umd.min.js @@ -0,0 +1,683 @@ +"use strict"; +(function (e, a) { + "object" === typeof exports && "undefined" !== typeof module + ? (module.exports = a()) + : "function" === typeof define && define.amd + ? define(a) + : ((e = "undefined" !== typeof globalThis ? globalThis : e || self), + (e.QrScanner = a())); +})(this, function () { + class e { + constructor(a, b, c, d, f) { + this._legacyCanvasSize = e.DEFAULT_CANVAS_SIZE; + this._preferredCamera = "environment"; + this._maxScansPerSecond = 25; + this._lastScanTimestamp = -1; + this._destroyed = this._flashOn = this._paused = this._active = !1; + this.$video = a; + this.$canvas = document.createElement("canvas"); + c && "object" === typeof c + ? (this._onDecode = b) + : (c || d || f + ? console.warn( + "You're using a deprecated version of the QrScanner constructor which will be removed in the future" + ) + : console.warn( + "Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true." + ), + (this._legacyOnDecode = b)); + b = "object" === typeof c ? c : {}; + this._onDecodeError = + b.onDecodeError || ("function" === typeof c ? c : this._onDecodeError); + this._calculateScanRegion = + b.calculateScanRegion || + ("function" === typeof d ? d : this._calculateScanRegion); + this._preferredCamera = b.preferredCamera || f || this._preferredCamera; + this._legacyCanvasSize = + "number" === typeof c + ? c + : "number" === typeof d + ? d + : this._legacyCanvasSize; + this._maxScansPerSecond = b.maxScansPerSecond || this._maxScansPerSecond; + this._onPlay = this._onPlay.bind(this); + this._onLoadedMetaData = this._onLoadedMetaData.bind(this); + this._onVisibilityChange = this._onVisibilityChange.bind(this); + this._updateOverlay = this._updateOverlay.bind(this); + a.disablePictureInPicture = !0; + a.playsInline = !0; + a.muted = !0; + let h = !1; + a.hidden && ((a.hidden = !1), (h = !0)); + document.body.contains(a) || (document.body.appendChild(a), (h = !0)); + c = a.parentElement; + if (b.highlightScanRegion || b.highlightCodeOutline) { + d = !!b.overlay; + this.$overlay = b.overlay || document.createElement("div"); + f = this.$overlay.style; + f.position = "absolute"; + f.display = "none"; + f.pointerEvents = "none"; + this.$overlay.classList.add("scan-region-highlight"); + if (!d && b.highlightScanRegion) { + this.$overlay.innerHTML = + ''; + try { + this.$overlay.firstElementChild.animate( + { transform: ["scale(.98)", "scale(1.01)"] }, + { + duration: 400, + iterations: Infinity, + direction: "alternate", + easing: "ease-in-out", + } + ); + } catch (m) {} + c.insertBefore(this.$overlay, this.$video.nextSibling); + } + b.highlightCodeOutline && + (this.$overlay.insertAdjacentHTML( + "beforeend", + '' + ), + (this.$codeOutlineHighlight = this.$overlay.lastElementChild)); + } + this._scanRegion = this._calculateScanRegion(a); + requestAnimationFrame(() => { + let m = window.getComputedStyle(a); + "none" === m.display && + (a.style.setProperty("display", "block", "important"), (h = !0)); + "visible" !== m.visibility && + (a.style.setProperty("visibility", "visible", "important"), + (h = !0)); + h && + (console.warn( + "QrScanner has overwritten the video hiding style to avoid Safari stopping the playback." + ), + (a.style.opacity = "0"), + (a.style.width = "0"), + (a.style.height = "0"), + this.$overlay && + this.$overlay.parentElement && + this.$overlay.parentElement.removeChild(this.$overlay), + delete this.$overlay, + delete this.$codeOutlineHighlight); + this.$overlay && this._updateOverlay(); + }); + a.addEventListener("play", this._onPlay); + a.addEventListener("loadedmetadata", this._onLoadedMetaData); + document.addEventListener("visibilitychange", this._onVisibilityChange); + window.addEventListener("resize", this._updateOverlay); + this._qrEnginePromise = e.createQrEngine(); + } + static set WORKER_PATH(a) { + console.warn( + "Setting QrScanner.WORKER_PATH is not required and not supported anymore. Have a look at the README for new setup instructions." + ); + } + static async hasCamera() { + try { + return !!(await e.listCameras(!1)).length; + } catch (a) { + return !1; + } + } + static async listCameras(a = !1) { + if (!navigator.mediaDevices) return []; + let b = async () => + (await navigator.mediaDevices.enumerateDevices()).filter( + (d) => "videoinput" === d.kind + ), + c; + try { + a && + (await b()).every((d) => !d.label) && + (c = await navigator.mediaDevices.getUserMedia({ + audio: !1, + video: !0, + })); + } catch (d) {} + try { + return (await b()).map((d, f) => ({ + id: d.deviceId, + label: d.label || (0 === f ? "Default Camera" : `Camera ${f + 1}`), + })); + } finally { + c && + (console.warn( + "Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream" + ), + e._stopVideoStream(c)); + } + } + async hasFlash() { + let a; + try { + if (this.$video.srcObject) { + if (!(this.$video.srcObject instanceof MediaStream)) return !1; + a = this.$video.srcObject; + } else a = (await this._getCameraStream()).stream; + return "torch" in a.getVideoTracks()[0].getSettings(); + } catch (b) { + return !1; + } finally { + a && + a !== this.$video.srcObject && + (console.warn( + "Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream" + ), + e._stopVideoStream(a)); + } + } + isFlashOn() { + return this._flashOn; + } + async toggleFlash() { + this._flashOn ? await this.turnFlashOff() : await this.turnFlashOn(); + } + async turnFlashOn() { + if ( + !this._flashOn && + !this._destroyed && + ((this._flashOn = !0), this._active && !this._paused) + ) + try { + if (!(await this.hasFlash())) throw "No flash available"; + await this.$video.srcObject + .getVideoTracks()[0] + .applyConstraints({ advanced: [{ torch: !0 }] }); + } catch (a) { + throw ((this._flashOn = !1), a); + } + } + async turnFlashOff() { + this._flashOn && ((this._flashOn = !1), await this._restartVideoStream()); + } + destroy() { + this.$video.removeEventListener("loadedmetadata", this._onLoadedMetaData); + this.$video.removeEventListener("play", this._onPlay); + document.removeEventListener("visibilitychange", this._onVisibilityChange); + window.removeEventListener("resize", this._updateOverlay); + this._destroyed = !0; + this._flashOn = !1; + this.stop(); + e._postWorkerMessage(this._qrEnginePromise, "close"); + } + async start() { + if (this._destroyed) + throw Error( + "The QR scanner can not be started as it had been destroyed." + ); + if (!this._active || this._paused) + if ( + ("https:" !== window.location.protocol && + console.warn( + "The camera stream is only accessible if the page is transferred via https." + ), + (this._active = !0), + !document.hidden) + ) + if (((this._paused = !1), this.$video.srcObject)) + await this.$video.play(); + else + try { + let { stream: a, facingMode: b } = + await this._getCameraStream(); + !this._active || this._paused + ? e._stopVideoStream(a) + : (this._setVideoMirror(b), + (this.$video.srcObject = a), + await this.$video.play(), + this._flashOn && + ((this._flashOn = !1), + this.turnFlashOn().catch(() => {}))); + } catch (a) { + if (!this._paused) throw ((this._active = !1), a); + } + } + stop() { + this.pause(); + this._active = !1; + } + async pause(a = !1) { + this._paused = !0; + if (!this._active) return !0; + this.$video.pause(); + this.$overlay && (this.$overlay.style.display = "none"); + let b = () => { + this.$video.srcObject instanceof MediaStream && + (e._stopVideoStream(this.$video.srcObject), + (this.$video.srcObject = null)); + }; + if (a) return b(), !0; + await new Promise((c) => setTimeout(c, 300)); + if (!this._paused) return !1; + b(); + return !0; + } + async setCamera(a) { + a !== this._preferredCamera && + ((this._preferredCamera = a), await this._restartVideoStream()); + } + static async scanImage(a, b, c, d, f = !1, h = !1) { + let m, + n = !1; + b && + ("scanRegion" in b || + "qrEngine" in b || + "canvas" in b || + "disallowCanvasResizing" in b || + "alsoTryWithoutScanRegion" in b || + "returnDetailedScanResult" in b) + ? ((m = b.scanRegion), + (c = b.qrEngine), + (d = b.canvas), + (f = b.disallowCanvasResizing || !1), + (h = b.alsoTryWithoutScanRegion || !1), + (n = !0)) + : b || c || d || f || h + ? console.warn( + "You're using a deprecated api for scanImage which will be removed in the future." + ) + : console.warn( + "Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true." + ); + b = !!c; + try { + let p, k; + [c, p] = await Promise.all([c || e.createQrEngine(), e._loadImage(a)]); + [d, k] = e._drawToCanvas(p, m, d, f); + let q; + if (c instanceof Worker) { + let g = c; + b || e._postWorkerMessageSync(g, "inversionMode", "both"); + q = await new Promise((l, v) => { + let w, + u, + r, + y = -1; + u = (t) => { + t.data.id === y && + (g.removeEventListener("message", u), + g.removeEventListener("error", r), + clearTimeout(w), + null !== t.data.data + ? l({ + data: t.data.data, + cornerPoints: e._convertPoints( + t.data.cornerPoints, + m + ), + }) + : v(e.NO_QR_CODE_FOUND)); + }; + r = (t) => { + g.removeEventListener("message", u); + g.removeEventListener("error", r); + clearTimeout(w); + v( + "Scanner error: " + + (t ? t.message || t : "Unknown Error") + ); + }; + g.addEventListener("message", u); + g.addEventListener("error", r); + w = setTimeout(() => r("timeout"), 1e4); + let x = k.getImageData(0, 0, d.width, d.height); + y = e._postWorkerMessageSync(g, "decode", x, [x.data.buffer]); + }); + } else + q = await Promise.race([ + new Promise((g, l) => + window.setTimeout(() => l("Scanner error: timeout"), 1e4) + ), + (async () => { + try { + var [g] = await c.detect(d); + if (!g) throw e.NO_QR_CODE_FOUND; + return { + data: g.rawValue, + cornerPoints: e._convertPoints(g.cornerPoints, m), + }; + } catch (l) { + g = l.message || l; + if (/not implemented|service unavailable/.test(g)) + return ( + (e._disableBarcodeDetector = !0), + e.scanImage(a, { + scanRegion: m, + canvas: d, + disallowCanvasResizing: f, + alsoTryWithoutScanRegion: h, + }) + ); + throw `Scanner error: ${g}`; + } + })(), + ]); + return n ? q : q.data; + } catch (p) { + if (!m || !h) throw p; + let k = await e.scanImage(a, { + qrEngine: c, + canvas: d, + disallowCanvasResizing: f, + }); + return n ? k : k.data; + } finally { + b || e._postWorkerMessage(c, "close"); + } + } + setGrayscaleWeights(a, b, c, d = !0) { + e._postWorkerMessage(this._qrEnginePromise, "grayscaleWeights", { + red: a, + green: b, + blue: c, + useIntegerApproximation: d, + }); + } + setInversionMode(a) { + e._postWorkerMessage(this._qrEnginePromise, "inversionMode", a); + } + static async createQrEngine(a) { + a && + console.warn( + "Specifying a worker path is not required and not supported anymore." + ); + a = () => + import("./qr-scanner-worker.min.js").then((c) => c.createWorker()); + if ( + !( + !e._disableBarcodeDetector && + "BarcodeDetector" in window && + BarcodeDetector.getSupportedFormats && + (await BarcodeDetector.getSupportedFormats()).includes("qr_code") + ) + ) + return a(); + let b = navigator.userAgentData; + return b && + b.brands.some(({ brand: c }) => /Chromium/i.test(c)) && + /mac ?OS/i.test(b.platform) && + (await b + .getHighEntropyValues(["architecture", "platformVersion"]) + .then( + ({ architecture: c, platformVersion: d }) => + /arm/i.test(c || "arm") && 13 <= parseInt(d || "13") + ) + .catch(() => !0)) + ? a() + : new BarcodeDetector({ formats: ["qr_code"] }); + } + _onPlay() { + this._scanRegion = this._calculateScanRegion(this.$video); + this._updateOverlay(); + this.$overlay && (this.$overlay.style.display = ""); + this._scanFrame(); + } + _onLoadedMetaData() { + this._scanRegion = this._calculateScanRegion(this.$video); + this._updateOverlay(); + } + _onVisibilityChange() { + document.hidden ? this.pause() : this._active && this.start(); + } + _calculateScanRegion(a) { + let b = Math.round((2 / 3) * Math.min(a.videoWidth, a.videoHeight)); + return { + x: Math.round((a.videoWidth - b) / 2), + y: Math.round((a.videoHeight - b) / 2), + width: b, + height: b, + downScaledWidth: this._legacyCanvasSize, + downScaledHeight: this._legacyCanvasSize, + }; + } + _updateOverlay() { + requestAnimationFrame(() => { + if (this.$overlay) { + var a = this.$video, + b = a.videoWidth, + c = a.videoHeight, + d = a.offsetWidth, + f = a.offsetHeight, + h = a.offsetLeft, + m = a.offsetTop, + n = window.getComputedStyle(a), + p = n.objectFit, + k = b / c, + q = d / f; + switch (p) { + case "none": + var g = b; + var l = c; + break; + case "fill": + g = d; + l = f; + break; + default: + ("cover" === p ? k > q : k < q) + ? ((l = f), (g = l * k)) + : ((g = d), (l = g / k)), + "scale-down" === p && + ((g = Math.min(g, b)), (l = Math.min(l, c))); + } + var [v, w] = n.objectPosition.split(" ").map((r, y) => { + const x = parseFloat(r); + return r.endsWith("%") ? ((y ? f - l : d - g) * x) / 100 : x; + }); + n = this._scanRegion.width || b; + q = this._scanRegion.height || c; + p = this._scanRegion.x || 0; + var u = this._scanRegion.y || 0; + k = this.$overlay.style; + k.width = `${(n / b) * g}px`; + k.height = `${(q / c) * l}px`; + k.top = `${m + w + (u / c) * l}px`; + c = /scaleX\(-1\)/.test(a.style.transform); + k.left = `${ + h + (c ? d - v - g : v) + ((c ? b - p - n : p) / b) * g + }px`; + k.transform = a.style.transform; + } + }); + } + static _convertPoints(a, b) { + if (!b) return a; + let c = b.x || 0, + d = b.y || 0, + f = b.width && b.downScaledWidth ? b.width / b.downScaledWidth : 1; + b = b.height && b.downScaledHeight ? b.height / b.downScaledHeight : 1; + for (let h of a) (h.x = h.x * f + c), (h.y = h.y * b + d); + return a; + } + _scanFrame() { + !this._active || + this.$video.paused || + this.$video.ended || + ("requestVideoFrameCallback" in this.$video + ? this.$video.requestVideoFrameCallback.bind(this.$video) + : requestAnimationFrame)(async () => { + if (!(1 >= this.$video.readyState)) { + var a = Date.now() - this._lastScanTimestamp, + b = 1e3 / this._maxScansPerSecond; + a < b && (await new Promise((d) => setTimeout(d, b - a))); + this._lastScanTimestamp = Date.now(); + try { + var c = await e.scanImage(this.$video, { + scanRegion: this._scanRegion, + qrEngine: this._qrEnginePromise, + canvas: this.$canvas, + }); + } catch (d) { + if (!this._active) return; + this._onDecodeError(d); + } + !e._disableBarcodeDetector || + (await this._qrEnginePromise) instanceof Worker || + (this._qrEnginePromise = e.createQrEngine()); + c + ? (this._onDecode + ? this._onDecode(c) + : this._legacyOnDecode && + this._legacyOnDecode(c.data), + this.$codeOutlineHighlight && + (clearTimeout( + this._codeOutlineHighlightRemovalTimeout + ), + (this._codeOutlineHighlightRemovalTimeout = void 0), + this.$codeOutlineHighlight.setAttribute( + "viewBox", + `${this._scanRegion.x || 0} ` + + `${this._scanRegion.y || 0} ` + + `${ + this._scanRegion.width || + this.$video.videoWidth + } ` + + `${ + this._scanRegion.height || + this.$video.videoHeight + }` + ), + this.$codeOutlineHighlight.firstElementChild.setAttribute( + "points", + c.cornerPoints + .map(({ x: d, y: f }) => `${d},${f}`) + .join(" ") + ), + (this.$codeOutlineHighlight.style.display = ""))) + : this.$codeOutlineHighlight && + !this._codeOutlineHighlightRemovalTimeout && + (this._codeOutlineHighlightRemovalTimeout = setTimeout( + () => + (this.$codeOutlineHighlight.style.display = + "none"), + 100 + )); + } + this._scanFrame(); + }); + } + _onDecodeError(a) { + a !== e.NO_QR_CODE_FOUND && console.log(a); + } + async _getCameraStream() { + if (!navigator.mediaDevices) throw "Camera not found."; + let a = /^(environment|user)$/.test(this._preferredCamera) + ? "facingMode" + : "deviceId", + b = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}], + c = b.map((d) => + Object.assign({}, d, { [a]: { exact: this._preferredCamera } }) + ); + for (let d of [...c, ...b]) + try { + let f = await navigator.mediaDevices.getUserMedia({ + video: d, + audio: !1, + }), + h = + this._getFacingMode(f) || + (d.facingMode + ? this._preferredCamera + : "environment" === this._preferredCamera + ? "user" + : "environment"); + return { stream: f, facingMode: h }; + } catch (f) {} + throw "Camera not found."; + } + async _restartVideoStream() { + let a = this._paused; + (await this.pause(!0)) && !a && this._active && (await this.start()); + } + static _stopVideoStream(a) { + for (let b of a.getTracks()) b.stop(), a.removeTrack(b); + } + _setVideoMirror(a) { + this.$video.style.transform = "scaleX(" + ("user" === a ? -1 : 1) + ")"; + } + _getFacingMode(a) { + return (a = a.getVideoTracks()[0]) + ? /rear|back|environment/i.test(a.label) + ? "environment" + : /front|user|face/i.test(a.label) + ? "user" + : null + : null; + } + static _drawToCanvas(a, b, c, d = !1) { + c = c || document.createElement("canvas"); + let f = b && b.x ? b.x : 0, + h = b && b.y ? b.y : 0, + m = b && b.width ? b.width : a.videoWidth || a.width, + n = b && b.height ? b.height : a.videoHeight || a.height; + d || + ((d = b && b.downScaledWidth ? b.downScaledWidth : m), + (b = b && b.downScaledHeight ? b.downScaledHeight : n), + c.width !== d && (c.width = d), + c.height !== b && (c.height = b)); + b = c.getContext("2d", { alpha: !1 }); + b.imageSmoothingEnabled = !1; + b.drawImage(a, f, h, m, n, 0, 0, c.width, c.height); + return [c, b]; + } + static async _loadImage(a) { + if (a instanceof Image) return await e._awaitImageLoad(a), a; + if ( + a instanceof HTMLVideoElement || + a instanceof HTMLCanvasElement || + a instanceof SVGImageElement || + ("OffscreenCanvas" in window && a instanceof OffscreenCanvas) || + ("ImageBitmap" in window && a instanceof ImageBitmap) + ) + return a; + if ( + a instanceof File || + a instanceof Blob || + a instanceof URL || + "string" === typeof a + ) { + let b = new Image(); + b.src = + a instanceof File || a instanceof Blob + ? URL.createObjectURL(a) + : a.toString(); + try { + return await e._awaitImageLoad(b), b; + } finally { + (a instanceof File || a instanceof Blob) && + URL.revokeObjectURL(b.src); + } + } else throw "Unsupported image type."; + } + static async _awaitImageLoad(a) { + (a.complete && 0 !== a.naturalWidth) || + (await new Promise((b, c) => { + let d = (f) => { + a.removeEventListener("load", d); + a.removeEventListener("error", d); + f instanceof ErrorEvent ? c("Image load error") : b(); + }; + a.addEventListener("load", d); + a.addEventListener("error", d); + })); + } + static async _postWorkerMessage(a, b, c, d) { + return e._postWorkerMessageSync(await a, b, c, d); + } + static _postWorkerMessageSync(a, b, c, d) { + if (!(a instanceof Worker)) return -1; + let f = e._workerMessageId++; + a.postMessage({ id: f, type: b, data: c }, d); + return f; + } + } + e.DEFAULT_CANVAS_SIZE = 400; + e.NO_QR_CODE_FOUND = "No QR code found"; + e._disableBarcodeDetector = !1; + e._workerMessageId = 0; + return e; +}); +//# sourceMappingURL=qr-scanner.umd.min.js.map diff --git a/hackathon_site/event/static/event/styles/scss/_variables.scss b/hackathon_site/event/static/event/styles/scss/_variables.scss index f56666829..a6f8b27e9 100644 --- a/hackathon_site/event/static/event/styles/scss/_variables.scss +++ b/hackathon_site/event/static/event/styles/scss/_variables.scss @@ -7,7 +7,13 @@ $colors: ( dark: #002f5e, backgroundColor: #ddeeff, error: #b00020, + errorBackground: #eed6da, success: #0b890b, + successBackground: #e4f1e4, + info: #044f9b, + infoBackground: #cfe1f1, + warning: #af5e00, + warningBackground: #e7d2b9, primaryOnHover: #08529c, secondaryOnHover: #ce8100, errorOnHover: #870000, diff --git a/hackathon_site/event/static/event/styles/scss/styles.scss b/hackathon_site/event/static/event/styles/scss/styles.scss index ce1079ac0..92da72e7c 100644 --- a/hackathon_site/event/static/event/styles/scss/styles.scss +++ b/hackathon_site/event/static/event/styles/scss/styles.scss @@ -85,7 +85,7 @@ strong { } .colorBtn { - font-weight: bold !important; + font-weight: 600 !important; text-align: center; text-transform: uppercase; color: color(font) !important; @@ -523,4 +523,38 @@ strong { margin: 5px 0 20px; } } + +.banner { + width: 100%; + padding: 10px; + text-align: center; + border-radius: 5px; + border: 2px; + margin: 10px 0; + + &success { + border: 1px solid color(success); + background-color: color(successBackground); + color: color(success); + } + + &error { + border: 1px solid color(error); + background-color: color(errorBackground); + color: color(error); + } + + &info { + border: 1px solid color(info); + background-color: color(infoBackground); + color: color(info); + } + + &warning { + border: 1px solid color(warning); + background-color: color(warningBackground); + color: color(warning); + } +} + // End of dashboard stuff diff --git a/hackathon_site/event/urls.py b/hackathon_site/event/urls.py index 2482c8fd8..9b1f7ed8d 100644 --- a/hackathon_site/event/urls.py +++ b/hackathon_site/event/urls.py @@ -1,6 +1,6 @@ from django.contrib.auth import views as auth_views from django.urls import path, reverse_lazy -from event.views import IndexView, DashboardView +from event.views import IndexView, DashboardView, QRScannerView from event.forms import ( PasswordChangeForm, PasswordResetForm, @@ -21,6 +21,7 @@ ), path("accounts/logout/", auth_views.LogoutView.as_view(), name="logout",), path("dashboard/", DashboardView.as_view(), name="dashboard"), + path("dashboard/qrscan/", QRScannerView.as_view(), name="qr-scanner"), path( "accounts/change_password/", auth_views.PasswordChangeView.as_view( diff --git a/hackathon_site/event/views.py b/hackathon_site/event/views.py index 11e86fe70..f3fdb6be3 100644 --- a/hackathon_site/event/views.py +++ b/hackathon_site/event/views.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction from django.shortcuts import redirect @@ -15,12 +16,16 @@ from rest_framework.filters import SearchFilter -from hackathon_site.utils import is_registration_open -from registration.forms import JoinTeamForm -from registration.models import Team as RegistrationTeam +from hackathon_site.utils import ( + is_registration_open, + is_hackathon_happening, + NoEventOccurringException, + get_curr_sign_in_time, +) +from registration.forms import JoinTeamForm, SignInForm +from registration.models import Team as RegistrationTeam, User, Application - -from event.models import Team as EventTeam +from event.models import Team as EventTeam, UserActivity from event.serializers import TeamSerializer from event.api_filters import TeamFilter from event.permissions import FullDjangoModelPermissions @@ -47,10 +52,14 @@ def get_context_data(self, **kwargs): class DashboardView(LoginRequiredMixin, FormView): - template_name = "event/dashboard_base.html" # Form submits should take the user back to the dashboard success_url = reverse_lazy("event:dashboard") + def get_template_names(self): + if self.request.user.is_staff: + return "event/dashboard_admin.html" + return "event/dashboard_base.html" + def get_form(self, form_class=None): """ The dashboard can have different forms, but not at the same time: @@ -188,6 +197,66 @@ def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) +class QRScannerView(LoginRequiredMixin, FormView): + success_url = reverse_lazy("event:qr-scanner") + + def get_template_names(self): + if self.request.user.is_staff: + return "event/admin_qr_scanner.html" + return Exception("You do not have permission to view this page.") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + if isinstance(context["form"], SignInForm): + context["sign_in_form"] = context["form"] + + return context + + def get_form(self, form_class=None): + if form_class is not None: + return form_class(**self.get_form_kwargs()) + + if is_hackathon_happening(): + return SignInForm(**self.get_form_kwargs()) + + return None + + def form_valid(self, form): + if isinstance(form, SignInForm): + try: + user = User.objects.get(email__exact=form.cleaned_data["email"]) + sign_in_event = get_curr_sign_in_time() + now = datetime.now().replace(tzinfo=settings.TZ_INFO) + + try: + user_activity = UserActivity.objects.get(user__exact=user) + setattr(user_activity, sign_in_event, now) + user_activity.save() + except UserActivity.DoesNotExist: + sign_in_obj = {} + sign_in_obj[sign_in_event] = now + UserActivity.objects.create(user=user, **sign_in_obj) + + messages.success( + self.request, + f'User {form.cleaned_data["email"]} successfully signed in', + ) + except NoEventOccurringException as e: + messages.info(self.request, str(e)) + except Exception as e: + messages.error( + self.request, + f'User {form.cleaned_data["email"]} could not sign in due to: {str(e)}', + ) + + return redirect(self.get_success_url()) + + @transaction.atomic + def post(self, request, *args, **kwargs): + return super().post(request, *args, **kwargs) + + class TeamListView(mixins.ListModelMixin, generics.GenericAPIView): queryset = EventTeam.objects.all() serializer_class = TeamSerializer diff --git a/hackathon_site/hackathon_site/jinja2.py b/hackathon_site/hackathon_site/jinja2.py index 43fc19c27..4ab224380 100644 --- a/hackathon_site/hackathon_site/jinja2.py +++ b/hackathon_site/hackathon_site/jinja2.py @@ -1,10 +1,16 @@ +from django.contrib import messages from django.contrib.staticfiles.storage import staticfiles_storage from django.conf import settings from django.urls import reverse from django.utils.timezone import template_localtime from jinja2 import Environment -from hackathon_site.utils import is_registration_open +from hackathon_site.utils import ( + is_registration_open, + get_sign_in_interval, + get_curr_sign_in_time, +) + # In testing, nothing in this file can be overwritten using the # @patch or @override_settings decorators, because it is evaluated before @@ -21,6 +27,9 @@ def environment(**options): "url": reverse, "localtime": template_localtime, "is_registration_open": is_registration_open, + "get_messages": messages.get_messages, + "get_sign_in_interval": get_sign_in_interval, + "get_curr_sign_in_time": get_curr_sign_in_time, # Variables "hackathon_name": settings.HACKATHON_NAME, "hss_url": settings.HSS_URL, @@ -37,6 +46,7 @@ def environment(**options): "chat_room_link": settings.CHAT_ROOM[1], "using_teams": settings.TEAMS, "using_rsvp": settings.RSVP, + "sign_in_times": settings.SIGN_IN_TIMES, } ) return env diff --git a/hackathon_site/hackathon_site/settings/__init__.py b/hackathon_site/hackathon_site/settings/__init__.py index ebfb97519..714005f16 100644 --- a/hackathon_site/hackathon_site/settings/__init__.py +++ b/hackathon_site/hackathon_site/settings/__init__.py @@ -92,6 +92,7 @@ "django_filters", "client_side_image_cropping", "captcha", + "qrcode", "dashboard", "registration", "event", @@ -146,6 +147,7 @@ LOGIN_REDIRECT_URL = reverse_lazy("event:dashboard") LOGOUT_REDIRECT_URL = reverse_lazy("event:index") +MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage" # Database # https://docs.djangoproject.com/en/3.0/ref/settings/#databases @@ -301,6 +303,36 @@ HARDWARE_SIGN_OUT_START_DATE = datetime(2020, 9, 1, tzinfo=TZ_INFO) HARDWARE_SIGN_OUT_END_DATE = datetime(2023, 9, 30, tzinfo=TZ_INFO) +# sign in times must be between EVENT_START_DATE and EVENT_END_DATE and in chronological order +# the number of sign in times MUST MATCH the number of columns in UserActivityTable +SIGN_IN_TIMES = [ + { + "name": "sign_in", + "description": "Hackathon Sign In", + "time": datetime(2023, 1, 13, 17, 0, 0, tzinfo=TZ_INFO), # Oct 10th @ 11am + }, + { + "name": "lunch1", + "description": "Lunch Day 1", + "time": datetime(2023, 1, 13, 22, 0, 0, tzinfo=TZ_INFO), # Oct 10th @ 2pm + }, + { + "name": "dinner1", + "description": "Dinner Day 1", + "time": datetime(2023, 1, 14, 6, 0, 0, tzinfo=TZ_INFO), # Oct 10th @ 6pm + }, + { + "name": "breakfast2", + "description": "Breakfast Day 2", + "time": datetime(2023, 1, 14, 11, 0, 0, tzinfo=TZ_INFO), # Oct 11th @ 9am + }, + { + "name": "lunch2", + "description": "Lunch Day 2", + "time": datetime(2023, 1, 14, 17, 0, 0, tzinfo=TZ_INFO), # Oct 11th @ 12pm + }, +] + # Registration user requirements MINIMUM_AGE = 14 diff --git a/hackathon_site/hackathon_site/utils.py b/hackathon_site/hackathon_site/utils.py index 209f65bab..5c8d54984 100644 --- a/hackathon_site/hackathon_site/utils.py +++ b/hackathon_site/hackathon_site/utils.py @@ -1,8 +1,16 @@ from datetime import datetime +from dateutil.relativedelta import relativedelta from django.conf import settings +class NoEventOccurringException(Exception): + def __init__(self): + super().__init__( + "There is currently no event happening for the user to sign in." + ) + + def is_registration_open(): """ Determine whether registration is currently open @@ -15,3 +23,35 @@ def is_registration_open(): # is configured to match TIME_ZONE. We then make the datetime object timezone-aware. now = datetime.now().replace(tzinfo=settings.TZ_INFO) return settings.REGISTRATION_OPEN_DATE <= now < settings.REGISTRATION_CLOSE_DATE + + +def is_hackathon_happening(): + if settings.IN_TESTING or settings.DEBUG: + return True + + now = datetime.now().replace(tzinfo=settings.TZ_INFO) + return settings.EVENT_START_DATE <= now < settings.EVENT_END_DATE + + +def get_curr_sign_in_time(useDescription=False): + now = datetime.now().replace(tzinfo=settings.TZ_INFO) + for event in settings.SIGN_IN_TIMES: + start_interval = event["time"] - relativedelta(hours=1) + end_interval = event["time"] + relativedelta(hours=1) + if start_interval <= now <= end_interval: + return event["description"] if useDescription else event["name"] + + if useDescription: + return None + raise NoEventOccurringException() + + +# assumes interval won't overlap between different months or years +def get_sign_in_interval(time): + start_interval = time - relativedelta(hours=1) + end_interval = time + relativedelta(hours=1) + + if start_interval.day == end_interval.day: + return f"{start_interval.strftime('%H:%M')} - {end_interval.strftime('%H:%M')}, {start_interval.strftime('%b %d')}" + + return f"{start_interval.strftime('%H:%M, %b %d')} - {end_interval.strftime('%H:%M, %b %d')}" diff --git a/hackathon_site/registration/forms.py b/hackathon_site/registration/forms.py index be9abf98c..87c2fe3c7 100644 --- a/hackathon_site/registration/forms.py +++ b/hackathon_site/registration/forms.py @@ -8,9 +8,10 @@ from django_registration import validators from django.conf import settings -from hackathon_site.utils import is_registration_open +from hackathon_site.utils import is_registration_open, is_hackathon_happening from registration.models import Application, Team, User from registration.widgets import MaterialFileInput +from review.models import Review class SignUpForm(UserCreationForm): @@ -211,3 +212,50 @@ def clean_team_code(self): raise forms.ValidationError(_(f"Team {team_code} is full.")) return team_code + + +class SignInForm(forms.Form): + email = forms.EmailField() + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.label_suffix = "" + self.error_css_class = "invalid" + + def clean(self): + if not is_hackathon_happening(): + raise forms.ValidationError( + _("You cannot sign in outside of the hackathon period."), + code="invalid_sign_in_time", + ) + + return super().clean() + + def clean_email(self): + email = self.cleaned_data["email"] + + try: + user = User.objects.get(email__exact=email) + application = Application.objects.get(user__exact=user) + review = Review.objects.get(application__exact=application) + if review.status == "Accepted": + if settings.RSVP and application.rsvp is None: + raise forms.ValidationError( + _(f"User {email} has not RSVP'd to the hackathon") + ) + else: + raise forms.ValidationError( + _( + f"User {email} has not been Accepted to attend {settings.HACKATHON_NAME}" + ) + ) + except User.DoesNotExist: + raise forms.ValidationError(_(f"User {email} does not exist.")) + except Application.DoesNotExist: + raise forms.ValidationError( + _(f"User {email} has not applied to {settings.HACKATHON_NAME}") + ) + except Exception as e: + raise e + + return email diff --git a/hackathon_site/requirements.txt b/hackathon_site/requirements.txt index d499015a4..f3ad7ebdd 100644 --- a/hackathon_site/requirements.txt +++ b/hackathon_site/requirements.txt @@ -32,6 +32,7 @@ python-dateutil==2.8.2 pyparsing==2.4.7 pytz==2020.1 PyYAML==5.4 +qrcode==7.3.1 redis==3.5.3 regex==2021.4.4 requests==2.25.1