qrcode: add tallying support (#86092)
This commit is contained in:
parent
38d3fbbf4e
commit
4738850fcc
|
@ -0,0 +1,46 @@
|
|||
# Generated by Django 3.2.18 on 2024-02-11 16:30
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('qrcode', '0005_auto_20231114_1747'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='reader',
|
||||
name='tally',
|
||||
field=models.BooleanField(default=False, verbose_name='Enable tally for this reader'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID')),
|
||||
('happened', models.DateTimeField()),
|
||||
('received', models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
'certificate',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='events',
|
||||
to='qrcode.certificate',
|
||||
),
|
||||
),
|
||||
(
|
||||
'reader',
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, related_name='events', to='qrcode.reader'
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,9 +1,10 @@
|
|||
import binascii
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from io import BytesIO
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
|
@ -53,6 +54,7 @@ READER_SCHEMA = {
|
|||
'title': _('Comma-separated list of metadata keys that this reader is allowed to read.'),
|
||||
'any': [{'type': 'null'}, {'const': ''}, {'type': 'string'}],
|
||||
},
|
||||
'enable_tallying': {'title': _('Enable tallying on this reader.'), 'type': 'boolean'},
|
||||
'validity_start': {
|
||||
'any': [{'type': 'null'}, {'const': ''}, {'type': 'string', 'format': 'date-time'}],
|
||||
},
|
||||
|
@ -70,6 +72,37 @@ def generate_key():
|
|||
|
||||
UUID_PATTERN = '(?P<uuid>[0-9|a-f]{8}-[0-9|a-f]{4}-[0-9|a-f]{4}-[0-9|a-f]{4}-[0-9a-f]{12})'
|
||||
|
||||
TALLY_SCHEMA = {
|
||||
'$schema': 'http://json-schema.org/draft-06/schema#',
|
||||
'type': 'object',
|
||||
'unflatten': True,
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'since': {
|
||||
'type': 'integer',
|
||||
'title': _('Get events since this timestamp'),
|
||||
},
|
||||
'events': {
|
||||
'type': 'array',
|
||||
'title': _('Events to tally'),
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'timestamp': {
|
||||
'type': 'integer',
|
||||
'title': _('Timestamp when the QRCode was read'),
|
||||
},
|
||||
'certificate': {
|
||||
'type': 'string',
|
||||
'title': _('Read certificate'),
|
||||
'pattern': UUID_PATTERN,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class QRCodeConnector(BaseResource):
|
||||
category = _('Misc')
|
||||
|
@ -213,12 +246,14 @@ class QRCodeConnector(BaseResource):
|
|||
validity_start=validity_start,
|
||||
validity_end=validity_end,
|
||||
readable_metadatas=readable_metadatas,
|
||||
tally=post_data.get('enable_tallying') or False,
|
||||
)
|
||||
else:
|
||||
reader = get_object_or_404(self.readers, uuid=uuid)
|
||||
reader.validity_start = validity_start
|
||||
reader.validity_end = validity_end
|
||||
reader.readable_metadatas = readable_metadatas
|
||||
reader.tally = post_data.get('enable_tallying') or False
|
||||
reader.save()
|
||||
|
||||
return {
|
||||
|
@ -278,6 +313,7 @@ class QRCodeConnector(BaseResource):
|
|||
'verify_key': self.hex_verify_key,
|
||||
'reader': reader,
|
||||
'metadata_url': reader.get_metadata_url(request),
|
||||
'tally_url': reader.get_tally_url(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -311,6 +347,55 @@ class QRCodeConnector(BaseResource):
|
|||
readable_metatadas = reader.readable_metadatas.split(',')
|
||||
return {'err': 0, 'data': {k: v for k, v in certificate.metadata.items() if k in readable_metatadas}}
|
||||
|
||||
@endpoint(
|
||||
name='tally',
|
||||
perm='OPEN',
|
||||
pattern=f'^{UUID_PATTERN}?$',
|
||||
description=_('Tally and get tallied events'),
|
||||
post={'request_body': {'schema': {'application/json': TALLY_SCHEMA}}},
|
||||
)
|
||||
def tally(self, request, uuid=None, post_data=None):
|
||||
reader = get_object_or_404(self.readers, uuid=uuid)
|
||||
|
||||
if not reader.tally:
|
||||
raise PermissionDenied('Tallying is not enabled for this reader')
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
since_timestamp = post_data.get('since')
|
||||
if since_timestamp == 0:
|
||||
since = now - timedelta(days=1)
|
||||
elif since_timestamp is not None:
|
||||
since = datetime.fromtimestamp(since_timestamp, timezone.utc) - timedelta(minutes=1)
|
||||
else:
|
||||
since = now
|
||||
|
||||
stamps = {}
|
||||
|
||||
for event in Event.objects.filter(received__gte=since):
|
||||
stamps[str(event.certificate.uuid)] = 'ok'
|
||||
|
||||
for event in post_data.get('events', []):
|
||||
try:
|
||||
certificate = self.certificates.get(uuid=event['certificate'])
|
||||
except Certificate.DoesNotExist:
|
||||
continue
|
||||
|
||||
_, created = Event.objects.get_or_create(
|
||||
certificate=certificate,
|
||||
defaults={
|
||||
'reader': reader,
|
||||
'happened': datetime.fromtimestamp(event['timestamp'], timezone.utc),
|
||||
},
|
||||
)
|
||||
stamps[str(certificate.uuid)] = 'ok' if created else 'duplicate'
|
||||
|
||||
return {
|
||||
'data': {
|
||||
'timestamp': int(datetime.timestamp(now)),
|
||||
'stamps': stamps,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def encode_mime_like(data):
|
||||
msg = ''
|
||||
|
@ -394,6 +479,7 @@ class Reader(models.Model):
|
|||
validity_end = models.DateTimeField(verbose_name=_('Validity End Date'), null=True)
|
||||
readable_metadatas = models.CharField(max_length=128, verbose_name=_('Readable metadata keys'), null=True)
|
||||
resource = models.ForeignKey(QRCodeConnector, on_delete=models.CASCADE, related_name='readers')
|
||||
tally = models.BooleanField(verbose_name=_('Enable tally for this reader'), default=False)
|
||||
|
||||
def get_url(self, request):
|
||||
return self._get_endpoint_url(request, 'open-reader')
|
||||
|
@ -403,6 +489,9 @@ class Reader(models.Model):
|
|||
return None
|
||||
return self._get_endpoint_url(request, 'read-metadata')
|
||||
|
||||
def get_tally_url(self, request):
|
||||
return self._get_endpoint_url(request, 'tally')
|
||||
|
||||
def _get_endpoint_url(self, request, endpoint):
|
||||
relative_url = reverse(
|
||||
'generic-endpoint',
|
||||
|
@ -414,3 +503,11 @@ class Reader(models.Model):
|
|||
},
|
||||
)
|
||||
return request.build_absolute_uri(relative_url)
|
||||
|
||||
|
||||
class Event(models.Model):
|
||||
uuid = models.UUIDField(verbose_name=_('UUID'), unique=True, default=uuid.uuid4)
|
||||
certificate = models.ForeignKey(Certificate, on_delete=models.CASCADE, related_name='events')
|
||||
reader = models.ForeignKey(Reader, on_delete=models.CASCADE, related_name='events')
|
||||
happened = models.DateTimeField()
|
||||
received = models.DateTimeField(auto_now_add=True)
|
||||
|
|
|
@ -147,6 +147,16 @@ qrcode-reader {
|
|||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
&--tally-status {
|
||||
display: grid;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
border-top: 1px solid var(--gray-light);
|
||||
&.error {
|
||||
color: #{$red};
|
||||
}
|
||||
}
|
||||
|
||||
&--spinner {
|
||||
grid-area: 1 / 1 / 2 / 3;
|
||||
justify-self: center;
|
||||
|
|
|
@ -63,6 +63,7 @@ const validityTemplate = template(`
|
|||
const dataTemplate = template(`
|
||||
<div class="qrcode-reader--data-items"></div>
|
||||
<span class="qrcode-reader--metadata-items"></span>
|
||||
<span class="qrcode-reader--tally-status"></span>
|
||||
`)
|
||||
|
||||
const dataItemTemplate = template(`
|
||||
|
@ -272,6 +273,7 @@ class QRCodeReader extends window.HTMLElement {
|
|||
|
||||
const dataItems = dataElement.content.querySelector('.qrcode-reader--data-items')
|
||||
const metadataItems = dataElement.content.querySelector('.qrcode-reader--metadata-items')
|
||||
const tallyStatus = dataElement.content.querySelector('.qrcode-reader--tally-status')
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
const dataItem = dataItemTemplate.cloneNode(true)
|
||||
|
@ -284,6 +286,10 @@ class QRCodeReader extends window.HTMLElement {
|
|||
if(this.getAttribute('metadata-url')) {
|
||||
this.#showMetadata(metadataItems, certificateUUID)
|
||||
}
|
||||
|
||||
if(this.getAttribute('tally-url')) {
|
||||
this.#showTally(tallyStatus, certificateUUID)
|
||||
}
|
||||
}
|
||||
|
||||
async #showMetadata(itemsElement, certificateUUID) {
|
||||
|
@ -333,6 +339,54 @@ class QRCodeReader extends window.HTMLElement {
|
|||
return metadata
|
||||
}
|
||||
|
||||
async #showTally(statusElement, certificateUUID) {
|
||||
const spinner = spinnerTemplate.cloneNode(true)
|
||||
statusElement.appendChild(spinner.content)
|
||||
|
||||
const tallyUrl = this.getAttribute('tally-url')
|
||||
try {
|
||||
const response = await fetch(
|
||||
tallyUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
events: [{
|
||||
certificate: certificateUUID,
|
||||
timestamp: Math.floor(Date.now() / 1000)}
|
||||
]}
|
||||
),
|
||||
})
|
||||
|
||||
statusElement.innerHTML = ''
|
||||
|
||||
if(!response.ok) {
|
||||
statusElement.classList.add('error')
|
||||
statusElement.innerHTML = `<p>${translate('tally_api_error')}</p>`
|
||||
}
|
||||
else {
|
||||
const json = await response.json()
|
||||
|
||||
if(json.data.stamps[certificateUUID] == 'duplicate') {
|
||||
statusElement.classList.add('error')
|
||||
statusElement.innerHTML = `<p>${translate('tally_status_already_seen')}</p>`
|
||||
}
|
||||
else {
|
||||
statusElement.innerHTML = `<p>${translate('tally_status_ok')}</p>`
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
if(err.name == 'NetworkError') {
|
||||
statusElement.classList.add('error')
|
||||
statusElement.innerHTML = `<p>${translate('tally_network_error')}</p>`
|
||||
}
|
||||
else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#showError (message) {
|
||||
this.#popup.classList.remove('closed')
|
||||
this.#popup.classList.add('error')
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
"never": "Jamais",
|
||||
"not_supported": "{% trans "QR code reader isn\'t supported on your platform. Please update your browser." %}",
|
||||
"not_yet_valid": "{% trans 'QR code not yet valid' %}",
|
||||
"tally_api_error": "{% trans 'An api error occured while tallying the QR code.' %}",
|
||||
"tally_status_already_seen": "{% trans 'This QR code has already been tallied.' %}",
|
||||
"tally_status_ok": "{% trans 'QR Code tallied.' %}",
|
||||
"metadata_network_error": "{% trans 'A network error occured while tallying the QR code.' %}",
|
||||
"to": "{% trans 'To' %}",
|
||||
"valid": "{% trans 'Valid QR code' %}"
|
||||
}
|
||||
|
@ -36,6 +40,7 @@
|
|||
{% else %}
|
||||
<qrcode-reader verify-key="{{ verify_key }}"
|
||||
{% if metadata_url %}metadata-url="{{metadata_url}}"{% endif %}
|
||||
{% if tally_url %}tally-url="{{tally_url}}"{% endif %}
|
||||
></qrcode-reader>
|
||||
{% endif %}
|
||||
</body>
|
||||
|
|
|
@ -346,3 +346,99 @@ qrcodeReaderMetadataTest('qrcode reader show feedback on api error', async ({moc
|
|||
expect(items.classList.contains('error')).toBe(true)
|
||||
expect(items.innerText.trim()).toBe('metadata_api_error')
|
||||
})
|
||||
|
||||
const qrcodeReaderTallyTest = qrcodeReaderTest.extend({
|
||||
loadTally: async ({ task, mock }, use) => {
|
||||
const loadTally = async (qrCodeData, mockFetch) => {
|
||||
const { reader, scan } = mock
|
||||
reader.setAttribute('tally-url', 'https://orson-welles.io')
|
||||
|
||||
let resolveLoadingEnded = undefined
|
||||
const loadingEnded = new Promise((resolve) => resolveLoadingEnded = resolve)
|
||||
fetch.mockImplementationOnce(async (url, {body}) => {
|
||||
const observer = new MutationObserver((_, observer) => {
|
||||
resolveLoadingEnded()
|
||||
observer.disconnect()
|
||||
})
|
||||
observer.observe(reader.querySelector(".qrcode-reader--tally-status"), { childList: true })
|
||||
return mockFetch(url, JSON.parse(body))
|
||||
})
|
||||
|
||||
await scan(qrCodeData)
|
||||
await loadingEnded
|
||||
}
|
||||
|
||||
await use(loadTally)
|
||||
}
|
||||
})
|
||||
|
||||
qrcodeReaderTallyTest('qrcode reader show tally status for unstamped certificates', async ({mock, loadTally}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
const nowBackup = Date.now
|
||||
Date.now = () => 25000
|
||||
await loadTally(okCodeData, async (url, body) => {
|
||||
expect(body).toStrictEqual({
|
||||
events: [{
|
||||
certificate: '853b9497-6c1a-4ff1-8604-6dc7cca9003c',
|
||||
timestamp: 25
|
||||
}]
|
||||
})
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'ok'}}})
|
||||
}
|
||||
})
|
||||
Date.now = nowBackup
|
||||
|
||||
const spinner = reader.querySelector('.qrcode-reader--spinner')
|
||||
expect(spinner).toBe(null)
|
||||
|
||||
const items = reader.querySelector('.qrcode-reader--tally-status')
|
||||
expect(items.classList.contains('error')).toBe(false)
|
||||
expect(items.innerText.trim()).toBe('tally_status_ok')
|
||||
})
|
||||
|
||||
qrcodeReaderTallyTest('qrcode reader show error for stamped certificates', async ({mock, loadTally}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
await loadTally(okCodeData, async (url, body) => {
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({'err': 0, 'data': {'stamps': {[body.events[0].certificate]: 'duplicate'}}})
|
||||
}
|
||||
})
|
||||
|
||||
const spinner = reader.querySelector('.qrcode-reader--spinner')
|
||||
expect(spinner).toBe(null)
|
||||
|
||||
const items = reader.querySelector('.qrcode-reader--tally-status')
|
||||
expect(items.classList.contains('error')).toBe(true)
|
||||
expect(items.innerText.trim()).toBe('tally_status_already_seen')
|
||||
})
|
||||
|
||||
qrcodeReaderTallyTest('qrcode reader show tally when request fails ', async ({mock, loadTally}) => {
|
||||
const { reader, scan } = mock
|
||||
|
||||
await loadTally(okCodeData, async (url, body) => {
|
||||
throw { name: 'NetworkError' }
|
||||
})
|
||||
|
||||
const spinner = reader.querySelector('.qrcode-reader--spinner')
|
||||
expect(spinner).toBe(null)
|
||||
|
||||
let items = reader.querySelector('.qrcode-reader--tally-status')
|
||||
expect(items.classList.contains('error')).toBe(true)
|
||||
expect(items.innerText.trim()).toBe('tally_network_error')
|
||||
|
||||
await loadTally(okCodeData, async (url, body) => {
|
||||
return {
|
||||
ok: false,
|
||||
}
|
||||
})
|
||||
|
||||
items = reader.querySelector('.qrcode-reader--tally-status')
|
||||
expect(items.classList.contains('error')).toBe(true)
|
||||
expect(items.innerText.trim()).toBe('tally_api_error')
|
||||
})
|
||||
|
||||
|
|
|
@ -13,13 +13,12 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import datetime
|
||||
import uuid
|
||||
from datetime import timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from passerelle.apps.qrcode.models import Certificate, QRCodeConnector, Reader
|
||||
from passerelle.apps.qrcode.models import Certificate, Event, QRCodeConnector, Reader
|
||||
from tests.utils import generic_endpoint_url, setup_access_rights
|
||||
|
||||
|
||||
|
@ -57,9 +56,10 @@ def test_save_certificate(app, connector):
|
|||
|
||||
assert certificate.data['first_name'] == 'Georges'
|
||||
assert certificate.data['last_name'] == 'Abitbol'
|
||||
|
||||
assert certificate.metadata['puissance_intellectuelle'] == 'BAC +2'
|
||||
assert certificate.validity_start == datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_end == datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_start == datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_end == datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{certificate_uuid}',
|
||||
|
@ -76,8 +76,8 @@ def test_save_certificate(app, connector):
|
|||
certificate.refresh_from_db()
|
||||
assert certificate.data['first_name'] == 'Robert'
|
||||
assert certificate.data['last_name'] == 'Redford'
|
||||
assert certificate.validity_start == datetime.datetime(2024, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_end == datetime.datetime(2025, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_start == datetime(2024, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert certificate.validity_end == datetime(2025, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_get_certificate(app, connector):
|
||||
|
@ -86,8 +86,8 @@ def test_get_certificate(app, connector):
|
|||
'first_name': 'Georges',
|
||||
'last_name': 'Abitbol',
|
||||
},
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'get-certificate', slug=connector.slug)
|
||||
|
@ -112,8 +112,8 @@ def test_get_qrcode(app, connector):
|
|||
'first_name': 'Georges',
|
||||
'last_name': 'Abitbol',
|
||||
},
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
endpoint = generic_endpoint_url('qrcode', 'get-qrcode', slug=connector.slug)
|
||||
|
||||
|
@ -133,6 +133,7 @@ def test_save_reader(app, connector):
|
|||
'validity_start': '2022-01-01 10:00:00+00:00',
|
||||
'validity_end': '2023-01-01 10:00:00+00:00',
|
||||
'readable_metadatas': 'name,last_name',
|
||||
'enable_tallying': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -142,9 +143,10 @@ def test_save_reader(app, connector):
|
|||
assert result.json['data']['url'] == f'http://testserver/qrcode/test/open-reader/{reader_uuid}'
|
||||
reader = connector.readers.get(uuid=reader_uuid)
|
||||
|
||||
assert reader.validity_start == datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_end == datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.readable_metadatas == 'name,last_name'
|
||||
assert reader.validity_start == datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_end == datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.tally
|
||||
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{reader_uuid}',
|
||||
|
@ -155,14 +157,15 @@ def test_save_reader(app, connector):
|
|||
)
|
||||
|
||||
reader.refresh_from_db()
|
||||
assert reader.validity_start == datetime.datetime(2024, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_end == datetime.datetime(2025, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_start == datetime(2024, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert reader.validity_end == datetime(2025, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc)
|
||||
assert not reader.tally
|
||||
|
||||
|
||||
def test_get_reader(app, connector):
|
||||
reader = connector.readers.create(
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'get-reader', slug=connector.slug)
|
||||
|
@ -181,8 +184,8 @@ def test_get_reader(app, connector):
|
|||
|
||||
def test_open_reader(app, connector, freezer):
|
||||
reader = connector.readers.create(
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'open-reader', slug=connector.slug)
|
||||
|
@ -196,6 +199,12 @@ def test_open_reader(app, connector, freezer):
|
|||
|
||||
assert result.pyquery(f'qrcode-reader[verify-key="{connector.hex_verify_key}"]')
|
||||
|
||||
reader.tally = True
|
||||
reader.save()
|
||||
result = app.get(f'{endpoint}/{reader.uuid}')
|
||||
|
||||
assert result.pyquery(f'qrcode-reader[verify-key="{connector.hex_verify_key}"][tally-url]')
|
||||
|
||||
freezer.move_to('2023-01-01T10:00:01')
|
||||
result = app.get(f'{endpoint}/{reader.uuid}')
|
||||
|
||||
|
@ -205,15 +214,15 @@ def test_open_reader(app, connector, freezer):
|
|||
def test_read_metadata(app, connector, freezer):
|
||||
reader = connector.readers.create(
|
||||
readable_metadatas='first_name,puissance_intellectuelle',
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
certificate = connector.certificates.create(
|
||||
uuid=uuid.UUID('12345678-1234-5678-1234-567812345678'),
|
||||
metadata={'first_name': 'Georges', 'last_name': 'Abitbol', 'puissance_intellectuelle': 'BAC +2'},
|
||||
validity_start=datetime.datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime.datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_start=datetime(2022, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
validity_end=datetime(2023, 1, 1, 10, 0, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'read-metadata', slug=connector.slug)
|
||||
|
@ -236,6 +245,118 @@ def test_read_metadata(app, connector, freezer):
|
|||
assert result.json['err_desc'] == 'Reader has expired.'
|
||||
|
||||
|
||||
def test_tally_query_events(app, connector, freezer):
|
||||
first_reader = connector.readers.create(tally=True)
|
||||
second_reader = connector.readers.create(tally=True)
|
||||
first_certificate = connector.certificates.create()
|
||||
second_certificate = connector.certificates.create()
|
||||
|
||||
freezer.move_to('2000-01-01T11:59:59')
|
||||
yesterday = datetime.now(timezone.utc)
|
||||
Event.objects.create(
|
||||
reader=first_reader,
|
||||
certificate=first_certificate,
|
||||
happened=yesterday - timedelta(minutes=1),
|
||||
)
|
||||
|
||||
freezer.move_to('2000-01-02T11:00:00')
|
||||
an_hour_ago = datetime.now(timezone.utc)
|
||||
Event.objects.create(
|
||||
reader=second_reader,
|
||||
certificate=second_certificate,
|
||||
# Far in the past to check that date taken into account is the received
|
||||
# date, not the happened one.
|
||||
happened=an_hour_ago - timedelta(days=1),
|
||||
)
|
||||
|
||||
freezer.move_to('2000-01-02T12:00:00')
|
||||
now = datetime.now(timezone.utc)
|
||||
endpoint = generic_endpoint_url('qrcode', 'tally', slug=connector.slug)
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{first_reader.uuid}',
|
||||
params={'since': 0, 'events': []},
|
||||
)
|
||||
assert result.json['err'] == 0
|
||||
assert result.json['data'] == {
|
||||
'timestamp': int(datetime.timestamp(now)),
|
||||
'stamps': {str(second_certificate.uuid): 'ok'},
|
||||
}
|
||||
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{first_reader.uuid}',
|
||||
params={
|
||||
'since': int(datetime.timestamp(yesterday)),
|
||||
},
|
||||
)
|
||||
|
||||
assert result.json['err'] == 0
|
||||
assert result.json['data'] == {
|
||||
'timestamp': int(datetime.timestamp(now)),
|
||||
'stamps': {
|
||||
str(first_certificate.uuid): 'ok',
|
||||
str(second_certificate.uuid): 'ok',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_tally_save_events(app, connector, freezer):
|
||||
freezer.move_to('2023-01-01T10:00:01')
|
||||
|
||||
reader = connector.readers.create(tally=True)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
a_minute_ago = now - timedelta(minutes=1)
|
||||
|
||||
certificate = connector.certificates.create()
|
||||
stamped_certificate = connector.certificates.create()
|
||||
|
||||
Event.objects.create(
|
||||
reader=reader,
|
||||
certificate=stamped_certificate,
|
||||
happened=a_minute_ago,
|
||||
)
|
||||
|
||||
endpoint = generic_endpoint_url('qrcode', 'tally', slug=connector.slug)
|
||||
result = app.post_json(
|
||||
f'{endpoint}/{reader.uuid}',
|
||||
params={
|
||||
'since': 0,
|
||||
'events': [
|
||||
{
|
||||
'certificate': str(certificate.uuid),
|
||||
'timestamp': datetime.timestamp(a_minute_ago),
|
||||
},
|
||||
{
|
||||
'certificate': 'deadbeef-0000-0000-0000-000000000000',
|
||||
'timestamp': datetime.timestamp(a_minute_ago),
|
||||
},
|
||||
{
|
||||
'certificate': str(stamped_certificate.uuid),
|
||||
'timestamp': datetime.timestamp(now),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert result.json['data'] == {
|
||||
'timestamp': int(datetime.timestamp(now)),
|
||||
'stamps': {
|
||||
str(certificate.uuid): 'ok',
|
||||
str(stamped_certificate.uuid): 'duplicate',
|
||||
},
|
||||
}
|
||||
|
||||
events = list(certificate.events.all())
|
||||
assert len(events) == 1
|
||||
assert events[0].reader == reader
|
||||
assert events[0].certificate == certificate
|
||||
assert events[0].happened == a_minute_ago
|
||||
assert events[0].received == now
|
||||
|
||||
events = list(stamped_certificate.events.all())
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
MISSING = object()
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue