diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e952b5..97e2c647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ml). ## master - [1.0.1-beta] + [1.0.2-beta] ## testing [1.0.3-beta] ## [1.0.3-beta] - [addend] #85 add mac of network adapter to device hid +- [addend] #95 adding endpoint for check the hash of one report - [changed] #94 change form of snapshot manual ## [1.0.2-beta] diff --git a/ereuse_devicehub/migrations/versions/3eb50297c365_add_hash.py b/ereuse_devicehub/migrations/versions/3eb50297c365_add_hash.py new file mode 100644 index 00000000..e6eff98d --- /dev/null +++ b/ereuse_devicehub/migrations/versions/3eb50297c365_add_hash.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: 3eb50297c365 +Revises: 378b6b147b46 +Create Date: 2020-12-18 16:26:15.453694 + +""" + +import citext +import sqlalchemy as sa + +from alembic import op +from alembic import context +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '3eb50297c365' +down_revision = '378b6b147b46' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + +def upgrade(): + # Report Hash table + op.create_table('report_hash', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created', sa.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), + nullable=False, comment='When Devicehub created this.'), + sa.Column('hash3', citext.CIText(), nullable=False), + sa.PrimaryKeyConstraint('id'), + schema=f'{get_inv()}' + ) + + +def downgrade(): + op.drop_table('report_hash', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/documents/documents.py b/ereuse_devicehub/resources/documents/documents.py index e65bee81..a7db64c6 100644 --- a/ereuse_devicehub/resources/documents/documents.py +++ b/ereuse_devicehub/resources/documents/documents.py @@ -11,19 +11,19 @@ import flask import flask_weasyprint import teal.marshmallow from boltons import urlutils -from flask import make_response, g +from flask import make_response, g, request +from flask.json import jsonify from teal.cache import cache -from teal.resource import Resource +from teal.resource import Resource, View from ereuse_devicehub.db import db from ereuse_devicehub.resources.action import models as evs from ereuse_devicehub.resources.device import models as devs from ereuse_devicehub.resources.device.views import DeviceView from ereuse_devicehub.resources.documents.device_row import DeviceRow, StockRow -from ereuse_devicehub.resources.documents.device_row import DeviceRow from ereuse_devicehub.resources.lot import LotView from ereuse_devicehub.resources.lot.models import Lot - +from ereuse_devicehub.resources.hash_reports import insert_hash, ReportHash class Format(enum.Enum): @@ -117,7 +117,7 @@ class DevicesDocumentView(DeviceView): def generate_post_csv(self, query): """Get device query and put information in csv format.""" data = StringIO() - cw = csv.writer(data, delimiter=';', quotechar='"') + cw = csv.writer(data, delimiter=';', lineterminator="\n", quotechar='"') first = True for device in query: d = DeviceRow(device) @@ -125,7 +125,9 @@ class DevicesDocumentView(DeviceView): cw.writerow(d.keys()) first = False cw.writerow(d.values()) - output = make_response(data.getvalue().encode('utf-8')) + bfile = data.getvalue().encode('utf-8') + output = make_response(bfile) + insert_hash(bfile) output.headers['Content-Disposition'] = 'attachment; filename=export.csv' output.headers['Content-type'] = 'text/csv' return output @@ -190,6 +192,19 @@ class StockDocumentView(DeviceView): return output +class CheckView(View): + model = ReportHash + + def get(self): + qry = dict(request.values) + hash3 = qry.get('hash') + + result = False + if hash3 and ReportHash.query.filter_by(hash3=hash3).count(): + result = True + return jsonify(result) + + class DocumentDef(Resource): __type__ = 'Document' SCHEMA = None @@ -238,3 +253,6 @@ class DocumentDef(Resource): stock_view = StockDocumentView.as_view('stockDocumentView', definition=self, auth=app.auth) stock_view = app.auth.requires_auth(stock_view) self.add_url_rule('/stock/', defaults=d, view_func=stock_view, methods=get) + + check_view = CheckView.as_view('CheckView', definition=self, auth=app.auth) + self.add_url_rule('/check/', defaults={}, view_func=check_view, methods=get) diff --git a/ereuse_devicehub/resources/hash_reports.py b/ereuse_devicehub/resources/hash_reports.py new file mode 100644 index 00000000..8822afca --- /dev/null +++ b/ereuse_devicehub/resources/hash_reports.py @@ -0,0 +1,29 @@ +"""Hash implementation and save in database +""" +import hashlib + +from citext import CIText +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import UUID +from uuid import uuid4 + +from ereuse_devicehub.db import db + + +class ReportHash(db.Model): + """Save the hash than is create when one report is download. + """ + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4) + id.comment = """The identifier of the device for this database. Used only + internally for software; users should not use this. + """ + hash3 = db.Column(CIText(), nullable=False) + hash3.comment = """The normalized name of the hash.""" + + +def insert_hash(bfile): + hash3 = hashlib.sha3_256(bfile).hexdigest() + db_hash = ReportHash(hash3=hash3) + db.session.add(db_hash) + db.session.commit() + db.session.flush() diff --git a/tests/test_basic.py b/tests/test_basic.py index 8c62a216..ea8bac0d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -53,6 +53,7 @@ def test_api_docs(client: Client): '/documents/lots/', '/documents/static/{filename}', '/documents/stock/', + '/documents/check/', '/drills/{dev1_id}/merge/{dev2_id}', '/graphic-cards/{dev1_id}/merge/{dev2_id}', '/hard-drives/{dev1_id}/merge/{dev2_id}', diff --git a/tests/test_documents.py b/tests/test_documents.py index 281ca65a..eecee5ad 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,4 +1,5 @@ import csv +import hashlib from datetime import datetime from io import StringIO from pathlib import Path @@ -15,7 +16,9 @@ from ereuse_devicehub.resources.documents import documents from ereuse_devicehub.resources.device import models as d from ereuse_devicehub.resources.lot.models import Lot from ereuse_devicehub.resources.tag.model import Tag +from ereuse_devicehub.resources.hash_reports import ReportHash from ereuse_devicehub.db import db +from tests import conftest from tests.conftest import file @@ -103,6 +106,7 @@ def test_export_csv_permitions(user: UserClient, user2: UserClient, client: Clie assert len(csv_user) > 0 assert len(csv_user2) == 0 + @pytest.mark.mvp def test_export_basic_snapshot(user: UserClient): """Test export device information in a csv file.""" @@ -128,6 +132,30 @@ def test_export_basic_snapshot(user: UserClient): assert fixture_csv[1][18] == export_csv[1][18], 'Computer information are not equal' assert fixture_csv[1][20:] == export_csv[1][20:], 'Computer information are not equal' + +@pytest.mark.mvp +@pytest.mark.usefixtures(conftest.app_context.__name__) +def test_check_insert_hash(app: Devicehub, user: UserClient, client: Client): + """Test export device information in a csv file.""" + snapshot, _ = user.post(file('basic.snapshot'), res=Snapshot) + csv_str, _ = user.get(res=documents.DocumentDef.t, + item='devices/', + accept='text/csv', + query=[('filter', {'type': ['Computer']})]) + hash3 = hashlib.sha3_256(csv_str.encode('utf-8')).hexdigest() + assert ReportHash.query.filter_by(hash3=hash3).count() == 1 + result, status = client.get(res=documents.DocumentDef.t, item='check/', query=[('hash', hash3)]) + assert status.status_code == 200 + assert result == True + + ff = open('/tmp/test.csv', 'w') + ff.write(csv_str) + ff.close() + + a= open('/tmp/test.csv').read() + assert hash3 == hashlib.sha3_256(a.encode('utf-8')).hexdigest() + + @pytest.mark.mvp def test_export_extended(app: Devicehub, user: UserClient): """Test a export device with all information and a lot of components."""