diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..57ad298b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +models/ +docker-volumes/ + diff --git a/.gitignore b/.gitignore index 5e7d7682..7a56c837 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,25 @@ __pycache__/ *.py[cod] *.ifc +*.json *.cover docker-volumes application/static/bimsurfer application/nix application/win application/ifc-pipeline.db +application/ifc-python-parser +application/ifc-parser +application/bimsurfer_temp/ +application/static/test.jpg +application/checks/ifcopenshell/ +application/checks/request_builder.py +application/checks/dresult_bsdd.json +application/checks/result_bsdd.json +application/checks/__pycache__ +application/checks/.pytest_cache +application/checks/tests/test_files +application/decoded.json +application/test_send.py +*.txt +application/ifcopenshell \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 9a2f5a54..74d79ff6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "application/bimsurfer"] path = application/bimsurfer url = https://github.com/AECgeeks/BIMsurfer2 +[submodule "application/checks/ifc-python-parser"] + path = application/checks/ifc-python-parser + url = https://github.com/johltn/ifc-python-parser +[submodule "application/checks/step-file-parser"] + path = application/checks/step-file-parser + url = https://github.com/IfcOpenShell/step-file-parser diff --git a/README.md b/README.md index e37bb294..de85c490 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,6 @@ There is a small web application in Flask that accepts file uploads. HTTPS is pr ./init.sh my.domain.name.com docker-compose up -d ~~~ + +## Development notes +Remember to store credentials as environment variables in `/etc/environment` diff --git a/application/Dockerfile b/application/Dockerfile index cd62f841..f5283818 100644 --- a/application/Dockerfile +++ b/application/Dockerfile @@ -1,8 +1,11 @@ FROM debian:buster-slim WORKDIR / -RUN apt-get -y update && apt-get -y --no-install-recommends --no-install-suggests install python3 python3-pip unzip wget libpq-dev build-essential libssl-dev libffi-dev libxml2-dev libxslt1-dev zlib1g-dev npm python3-setuptools python3-dev python3-wheel supervisor -RUN python3 -m pip install flask flask-cors numpy Pillow gunicorn rq redis sqlalchemy psycopg2 sqlalchemy-utils Flask-BasicAuth flasgger requests flask-dropzone +RUN apt-get -y update && apt-get -y --no-install-recommends --no-install-suggests install git python3 python3-pip unzip wget libpq-dev build-essential libssl-dev libffi-dev libxml2-dev libxslt1-dev zlib1g-dev npm python3-setuptools python3-dev python3-wheel supervisor + +RUN python3 -m pip install --upgrade pip + +RUN python3 -m pip install flask flask-cors numpy Pillow gunicorn rq redis SQLAlchemy==1.3.24 psycopg2 sqlalchemy-utils Flask-BasicAuth flasgger requests flask-dropzone lark-parser pyparsing XlsxWriter fuzzywuzzy python-Levenshtein requests_oauthlib Authlib requests argparse --upgrade RUN npm install -g jsdoc gltf-pipeline requirejs npx uglify-js # IfcConvert v0.6.0 @@ -13,6 +16,7 @@ RUN wget -O /tmp/ifcopenshell_python.zip https://s3.amazonaws.com/ifcopenshell-b RUN mkdir -p `python3 -c 'import site; print(site.getusersitepackages())'` RUN unzip -d `python3 -c 'import site; print(site.getusersitepackages())'` /tmp/ifcopenshell_python.zip + # Temporary 'hotfix' RUN wget -O `python3 -c 'import site; print(site.getusersitepackages())'`/ifcopenshell/validate.py https://raw.githubusercontent.com/IfcOpenShell/IfcOpenShell/v0.6.0/src/ifcopenshell-python/ifcopenshell/validate.py @@ -21,6 +25,18 @@ WORKDIR /www COPY application/*.py /www/ COPY application/templates /www/templates +COPY application/checks /www/checks +RUN unzip -d /www/checks /tmp/ifcopenshell_python.zip + + +RUN mkdir -p /www/checks/ifcopenshell/mvd +RUN echo 1 +RUN git clone https://github.com/opensourceBIM/python-mvdxml /www/checks/ifcopenshell/mvd + +# RUN ls www/checks/ifcopenshell/ +# RUN mv python-mvdxml/* www/checks/ifcopenshell/mvd +# RUN ls www/checks/ifcopenshell/ + COPY .git/HEAD /tmp/.git/HEAD COPY .git/refs/ /tmp/.git/refs/ RUN /bin/bash -c '(cat /tmp/.git/$(cat /tmp/.git/HEAD | cut -d \ -f 2)) || cat /tmp/.git/HEAD' > /version diff --git a/application/bimsurfer b/application/bimsurfer index 657070e1..99eced66 160000 --- a/application/bimsurfer +++ b/application/bimsurfer @@ -1 +1 @@ -Subproject commit 657070e10464059abf26b9e75684e66015f8fc9f +Subproject commit 99eced669e97c45cb5732912bdbd1d66754e49d3 diff --git a/application/checks/check_MVD.py b/application/checks/check_MVD.py new file mode 100644 index 00000000..50cfd3ea --- /dev/null +++ b/application/checks/check_MVD.py @@ -0,0 +1,58 @@ +import ifcopenshell +from ifcopenshell.mvd import mvd +import logging +import json +import sys +import os +import time + +def validate_mvd(mvd_fn): + mvd_concept_roots = ifcopenshell.mvd.concept_root.parse(mvd_fn) + passed = 1 + + for concept_root in mvd_concept_roots: + try: #todo: check mvdXML file schema + entity_type = concept_root.entity + if len(ifc_file.by_type(entity_type)): + entity_instances = ifc_file.by_type(entity_type) + for concept in concept_root.concepts(): + for rule in concept.template().rules: + + for e in entity_instances: + extraction = mvd.extract_data(rule,e) + for ex in extraction: + print(ex) + for k, v in ex.items(): + print(v) + if v == "Nonexistent value": + passed = 0 + + except: + pass + + return passed + + +if __name__ == "__main__": + start_time = time.time() + + ifc_fn = sys.argv[1] + ifc_file = ifcopenshell.open(ifc_fn) + + mvd_fn = "./ifcopenshell/mvd/mvd_examples/officials/ReferenceView_V1-2.mvdxml" + mvd_fn= os.path.join(os.path.dirname(__file__), "ifcopenshell/mvd/mvd_examples/officials/ReferenceView_V1-2.mvdxml") + mvd_concept_roots = ifcopenshell.mvd.concept_root.parse(mvd_fn) + + jsonresultout = os.path.join(os.getcwd(), "result_mvd.json") + passed = validate_mvd(mvd_fn) + print("--- %s seconds ---" % (time.time() - start_time)) + + if passed == 1: + mvd_result = {'mvd':'v'} + elif passed == 0: + mvd_result = {'mvd':'i'} + + + with open(jsonresultout, 'w', encoding='utf-8') as f: + json.dump(mvd_result, f, ensure_ascii=False, indent=4) + diff --git a/application/checks/check_bsdd_v2.py b/application/checks/check_bsdd_v2.py new file mode 100644 index 00000000..5008e536 --- /dev/null +++ b/application/checks/check_bsdd_v2.py @@ -0,0 +1,158 @@ +import ifcopenshell +import sys +import requests +import json +import argparse +from helper import database + + + +def get_classification_object(uri): + url = "https://bs-dd-api-prototype.azurewebsites.net/api/Classification/v3" + return requests.get(url, {'namespaceUri':uri}) + +def validate_ifc_classification_reference(relating_classification): + uri = relating_classification.Location + bsdd_response = get_classification_object(uri) + if bsdd_response.status_code != 200: + return 0 + elif bsdd_response.status_code == 200: + return bsdd_response + +def has_specifications(bsdd_response_content): + if bsdd_response_content["classificationProperties"]: + return 1 + else: + return 0 + +def validate_instance(constraint,ifc_file, instance): + + result = {"pset_name":"pset not found","property_name":"pset not found","value":"pset not found","datatype":"pset not found" } + constraint = { + "specified_pset_name":constraint["propertySet"], + "specified_property_name" : constraint["name"], + "specified_datatype" : constraint["dataType"], + "specified_predefined_value" : constraint["predefinedValue"], + } + + + # Integrate these: + # "maxExclusive": 0 + # "maxInclusive": 0 + # "minExclusive": 0 + # "minInclusive": 0 + # "pattern": "" + + for definition in instance.IsDefinedBy: + if definition.is_a() == "IfcRelDefinesByProperties": + + pset = definition.RelatingPropertyDefinition + if pset.Name == constraint["specified_pset_name"]: + result["property_name"] = "property not found" + result["value"] = "property not found" + result["datatype"] = "property not found" + + result = {"pset_name":pset.Name,"property_name":"pset not found","value":"pset not found","datatype":"pset not found" } + for property in pset.HasProperties: + if property.Name == constraint["specified_property_name"]: + result["property_name"] = property.Name + + if isinstance(property.NominalValue, ifcopenshell.entity_instance): + result["value"] = property.NominalValue[0] + result["datatype"] = type(property.NominalValue[0]) + else: + result["value"] = property.NominalValue + result["datatype"] = type(property.NominalValue[0]) + + + + return {"constraint":constraint,"result":result} + + + +def check_bsdd(ifc_fn, task_id): + + file_code = ifc_fn.split(".ifc")[0] + ifc_file = ifcopenshell.open(ifc_fn) + + with database.Session() as session: + model = session.query(database.model).filter(database.model.code == file_code)[0] + file_id = model.id + + n = len(ifc_file.by_type("IfcRelAssociatesClassification")) + if n: + percentages = [i * 100. / n for i in range(n+1)] + num_dots = [int(b) - int(a) for a, b in zip(percentages, percentages[1:])] + + for idx, rel in enumerate(ifc_file.by_type("IfcRelAssociatesClassification")): + + sys.stdout.write(num_dots[idx] * ".") + sys.stdout.flush() + + related_objects = rel.RelatedObjects + relating_classification = rel.RelatingClassification + + bsdd_response = validate_ifc_classification_reference(relating_classification) + bsdd_content = json.loads(bsdd_response.text) + + for ifc_instance in related_objects: + instance = database.ifc_instance(ifc_instance.GlobalId, ifc_instance.is_a(), file_id) + session.add(instance) + session.flush() + instance_id = instance.id + session.commit() + + if bsdd_response: + if has_specifications(bsdd_content): + specifications = bsdd_content["classificationProperties"] + for constraint in specifications: + bsdd_result = database.bsdd_result(task_id) + # Should create instance entry + bsdd_result.instance_id = instance_id + + bsdd_result.bsdd_classification_uri = bsdd_content["namespaceUri"] + bsdd_result.bsdd_type_constraint = ";".join(bsdd_content["relatedIfcEntityNames"]) + bsdd_result.bsdd_property_constraint = json.dumps(constraint) + bsdd_result.bsdd_property_uri = constraint["propertyNamespaceUri"] + + results = validate_instance(constraint, ifc_file, ifc_instance)["result"] + + bsdd_result.ifc_property_set = results["pset_name"] + bsdd_result.ifc_property_name = results["property_name"] + + if not isinstance(results["datatype"], str): + bsdd_result.ifc_property_type = results["datatype"].__name__ + bsdd_result.ifc_property_value = results["value"] + + session.add(bsdd_result) + session.commit() + else: + # Record NULL in other fields + bsdd_result = database.bsdd_result(task_id) + bsdd_result.bsdd_property_constraint = "no constraint" + session.add(bsdd_result) + session.commit() + else: + # Record NULL everywhere in bsdd_result + bsdd_result = database.bsdd_result(task_id) + bsdd_result.bsdd_classification_uri = "classification not found" + session.add(bsdd_result) + session.commit() + + #todo: implement scores that actually validate or not the model + model = session.query(database.model).filter(database.model.code == file_code)[0] + model.status_bsdd = 'v' + session.commit() + +if __name__=="__main__": + parser = argparse.ArgumentParser(description="Generate classified IFC file") + parser.add_argument("--input","-i", default="Duplex_A_20110505.ifc", type=str) + parser.add_argument("--task","-t", default=0, type=int) + + args = parser.parse_args() + check_bsdd(args.input, args.task) + + + + + diff --git a/application/checks/helper.py b/application/checks/helper.py new file mode 100644 index 00000000..e417cdfa --- /dev/null +++ b/application/checks/helper.py @@ -0,0 +1,5 @@ +import os, sys + +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +import database diff --git a/application/checks/ifc-python-parser b/application/checks/ifc-python-parser new file mode 160000 index 00000000..a7143858 --- /dev/null +++ b/application/checks/ifc-python-parser @@ -0,0 +1 @@ +Subproject commit a71438584a81c22ad44d2b72ac2d35ee508e3167 diff --git a/application/checks/info.py b/application/checks/info.py new file mode 100644 index 00000000..115cfd60 --- /dev/null +++ b/application/checks/info.py @@ -0,0 +1,33 @@ +import sys, os +import ifcopenshell +from helper import database + +ifc_fn = sys.argv[1] +ifc_file = ifcopenshell.open(ifc_fn) + +try: + detected_mvd = ifc_file.header.file_description.description[0].split(" ", 1)[1] + detected_mvd = detected_mvd[1:-1] +except: + detected_mvd = "no MVD detected" + +try: + authoring_app = ifc_file.by_type("IfcApplication")[0].ApplicationFullName +except: + authoring_app = 'no authoring app detected' + +with database.Session() as session: + model = session.query(database.model).filter(database.model.code == ifc_fn[:-4]).all()[0] + model.size = str(round(os.path.getsize(ifc_fn)*10**-6)) + "MB" + model.schema = ifc_file.schema + model.authoring_application = authoring_app + model.mvd = detected_mvd + model.number_of_elements = len(ifc_file.by_type("IfcBuildingElement")) + model.number_of_geometries = len(ifc_file.by_type("IfcShapeRepresentation")) + model.number_of_properties = len(ifc_file.by_type("IfcProperty")) + + session.commit() + session.close() + + + diff --git a/application/checks/tests/test_bsdd.py b/application/checks/tests/test_bsdd.py new file mode 100644 index 00000000..0f5e2ef7 --- /dev/null +++ b/application/checks/tests/test_bsdd.py @@ -0,0 +1,11 @@ +import pytest +import sys +import os + +path = os.path.join(os.path.dirname(__file__), "..") +sys.path.insert(1, path) + +from check_bSDD import get_domains + +def test_domains(): + assert len(get_domains()) == 18 diff --git a/application/checks/tests/test_parser.py b/application/checks/tests/test_parser.py new file mode 100644 index 00000000..98ca1ce6 --- /dev/null +++ b/application/checks/tests/test_parser.py @@ -0,0 +1,21 @@ +import pytest +import sys +import os + +import ifcopenshell + +path = os.path.join(os.path.dirname(__file__), "../step-file-parser") +sys.path.insert(1, path) + +from parse_file import * + +def test_parsing(): + filepath = os.path.join(os.path.dirname(__file__), "test_files\\Duplex_A_20110505.ifc") + ifc_file = ifcopenshell.open(filepath) + + f = open(filepath, "r") + text = f.read() + tree = ifc_parser.parse(text) + entities = process_tree(tree) + + assert entities[375]['attributes'][0] == ifc_file.by_id(375).GlobalId diff --git a/application/database.py b/application/database.py index 2fe50286..432fcab4 100644 --- a/application/database.py +++ b/application/database.py @@ -27,27 +27,58 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.sql import func from sqlalchemy.inspection import inspect -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy import Column, Integer, Float, String, DateTime, ForeignKey, Enum from sqlalchemy_utils import database_exists, create_database from sqlalchemy.orm import relationship import os +import datetime DEVELOPMENT = os.environ.get('environment', 'production').lower() == 'development' if DEVELOPMENT: - engine = create_engine('sqlite:///ifc-pipeline.db', connect_args={'check_same_thread': False}) + file_path = os.path.join(os.path.dirname(__file__), "ifc-pipeline.db") + engine = create_engine(f'sqlite:///{file_path}', connect_args={'check_same_thread': False}) else: - engine = create_engine('postgresql://postgres:postgres@%s:5432/bimsurfer2' % os.environ.get('POSTGRES_HOST', 'localhost')) - + host = os.environ.get('POSTGRES_HOST', 'localhost') + password = os.environ['POSTGRES_PASSWORD'] + engine = create_engine(f"postgresql://postgres:{password}@{host}:5432/bimsurfer2") + Session = sessionmaker(bind=engine) Base = declarative_base() - class Serializable(object): def serialize(self): - return {c: getattr(self, c) for c in inspect(self).attrs.keys()} - + # Transforms data from dataclasses to a dict, + # storing primary key of references and handling date format + d = {} + for attribute in inspect(self).attrs.keys(): + if isinstance(getattr(self, attribute), (list, tuple)): + d[attribute] = [element.id for element in getattr(self, attribute)] + elif isinstance(getattr(self, attribute), datetime.datetime): + d[attribute] = getattr(self, attribute).strftime("%Y-%m-%d %H:%M:%S") + else: + d[attribute] = getattr(self, attribute) + return d + + +class user(Base, Serializable): + __tablename__ = 'users' + + id = Column(String, primary_key=True) + email = Column(String) + family_name = Column(String) + given_name = Column(String) + name = Column(String) + models = relationship("model") + + def __init__(self, id, email, family_name, given_name, name): + self.id = id + self.email = email + self.family_name = family_name + self.given_name = given_name + self.name = name + class model(Base, Serializable): __tablename__ = 'models' @@ -55,16 +86,36 @@ class model(Base, Serializable): id = Column(Integer, primary_key=True) code = Column(String) filename = Column(String) - files = relationship("file") - + user_id = Column(String, ForeignKey('users.id')) + progress = Column(Integer, default=-1) date = Column(DateTime, server_default=func.now()) - def __init__(self, code, filename): - self.code = code - self.filename = filename + license = Column(Enum('private','CC','MIT','GPL','LGPL'), server_default="private") + hours = Column(Float) + details = Column(String) + number_of_elements = Column(Integer) + number_of_geometries = Column(Integer) + number_of_properties = Column(Integer) + + authoring_application = Column(String) + schema = Column(String) + size = Column(String) + mvd = Column(String) + + status_syntax = Column(Enum('n','v','w','i'), default='n') + status_schema = Column(Enum('n','v','w','i'), default='n') + status_bsdd = Column(Enum('n','v','w','i'), default='n') + status_mvd = Column(Enum('n','v','w','i'), default='n') + status_ids= Column(Enum('n','v','w','i'), default='n') + + instances = relationship("ifc_instance") + def __init__(self, code, filename, user_id): + self.code = code + self.filename = filename + self.user_id = user_id class file(Base, Serializable): @@ -74,18 +125,59 @@ class file(Base, Serializable): code = Column(String) filename = Column(String) - model_id = Column(Integer, ForeignKey('models.id')) - - progress = Column(Integer, default=-1) - date = Column(DateTime, server_default=func.now()) - def __init__(self, code, filename): self.code = code self.filename = filename + + +class bsdd_validation_task(Base, Serializable): + __tablename__ = 'bSDD_validation_tasks' + + id = Column(Integer, primary_key=True) + validated_file = Column(Integer, ForeignKey('models.id')) + validation_start_time = Column(DateTime) + validation_end_time = Column(DateTime) + + results = relationship("bsdd_result") + + def __init__(self, validated_file): + self.validated_file = validated_file - self.model_id = Column(Integer, ForeignKey('models.id')) +class ifc_instance(Base, Serializable): + __tablename__ = 'instances' + id = Column(Integer, primary_key=True) + global_id = Column(String) + file = Column(Integer, ForeignKey('models.id')) + ifc_type = Column(String) + bsdd_results = relationship("bsdd_result") + + def __init__(self, global_id, ifc_type, file): + self.global_id = global_id + self.ifc_type = ifc_type + self.file = file + + +class bsdd_result(Base, Serializable): + __tablename__ = 'bSDD_results' + + id = Column(Integer, primary_key=True) + task_id = Column(Integer, ForeignKey('bSDD_validation_tasks.id')) + instance_id = Column(Integer, ForeignKey('instances.id')) + bsdd_classification_uri = Column(String) + bsdd_property_uri = Column(String) + bsdd_property_constraint = Column(String) + bsdd_type_constraint = Column(String) + ifc_property_set = Column(String) + ifc_property_name = Column(String) + ifc_property_type = Column(String) + ifc_property_value = Column(String) + + + def __init__(self, task_id): + self.task_id = task_id + def initialize(): if not database_exists(engine.url): create_database(engine.url) diff --git a/application/defs.yml b/application/defs.yml new file mode 100644 index 00000000..37ea603c --- /dev/null +++ b/application/defs.yml @@ -0,0 +1,28 @@ +Validation using JsonSchema for checking the config +--- +tags: + - config +parameters: + - name: body + in: formData + required: true + schema: + id: User + required: + - syntax + - schema + - mvd + - bsdd + properties: + syntax: + type: string + default: "0" + schema: + type: string + default: "0" + mvd: + type: string + default: "0" + bsdd: + type: string + default: "0" diff --git a/application/main.py b/application/main.py index d234479f..26a0440c 100644 --- a/application/main.py +++ b/application/main.py @@ -1,3 +1,4 @@ + ################################################################################## # # # Copyright (c) 2020 AECgeeks # @@ -24,31 +25,55 @@ from __future__ import print_function + import os import json +import ast import threading +from functools import wraps -from collections import defaultdict, namedtuple -from flask_dropzone import Dropzone +from collections import namedtuple +from redis.client import parse_client_list from werkzeug.middleware.proxy_fix import ProxyFix -from flask import Flask, request, send_file, render_template, abort, jsonify, redirect, url_for, make_response +from flask import Flask, request, session, send_file, render_template, abort, jsonify, redirect, url_for, make_response from flask_cors import CORS -from flask_basicauth import BasicAuth -from flasgger import Swagger + +from flasgger import Swagger, validate + +import requests +from requests_oauthlib import OAuth2Session +from authlib.jose import jwt import utils -import worker import database +import worker + + +def send_simple_message(msg_content): + dom = os.getenv("MG_DOMAIN") + base_url = f"https://api.mailgun.net/v3/{dom}/messages" + from_ = f"Validation Service " + email = os.getenv("MG_EMAIL") + + return requests.post( + base_url, + auth=("api", os.getenv("MG_KEY")), + + data={"from": from_, + "to": [email], + "subject": "License update", + "text": msg_content}) + application = Flask(__name__) -dropzone = Dropzone(application) -# application.config['DROPZONE_UPLOAD_MULTIPLE'] = True -# application.config['DROPZONE_PARALLEL_UPLOADS'] = 3 -DEVELOPMENT = os.environ.get('environment', 'production').lower() == 'development' +DEVELOPMENT = os.environ.get( + 'environment', 'production').lower() == 'development' +VALIDATION = 1 +VIEWER = 0 if not DEVELOPMENT and os.path.exists("/version"): PIPELINE_POSTFIX = "." + open("/version").read().strip() @@ -80,141 +105,330 @@ from redis import Redis from rq import Queue - q = Queue(connection=Redis(host=os.environ.get("REDIS_HOST", "localhost")), default_timeout=3600) + q = Queue(connection=Redis(host=os.environ.get( + "REDIS_HOST", "localhost")), default_timeout=3600) + + +if not DEVELOPMENT: + application.config['SESSION_TYPE'] = 'filesystem' + application.config['SECRET_KEY'] = os.environ['SECRET_KEY'] + # LOGIN + os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" + os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" + # Credentials you get from registering a new application + client_id = os.environ['CLIENT_ID'] + client_secret = os.environ['CLIENT_SECRET'] + authorization_base_url = 'https://buildingsmartservices.b2clogin.com/buildingsmartservices.onmicrosoft.com/b2c_1a_signupsignin_c/oauth2/v2.0/authorize' + token_url = 'https://buildingSMARTservices.b2clogin.com/buildingSMARTservices.onmicrosoft.com/b2c_1a_signupsignin_c/oauth2/v2.0/token' + + redirect_uri = 'https://validate-bsi-staging.aecgeeks.com/callback' + + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if not DEVELOPMENT: + if not "oauth_token" in session.keys(): + return redirect(url_for('login')) + return f(session['decoded'],*args, **kwargs) + else: + with open('decoded.json') as json_file: + decoded = json.load(json_file) + return f(decoded, *args, **kwargs) + return decorated_function + + +@application.route("/") +@login_required +def index(decoded): + return render_template('index.html', decoded=decoded) + + +@application.route('/login', methods=['GET']) +def login(): + bs = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=[ + "openid profile", "https://buildingSMARTservices.onmicrosoft.com/api/read"]) + authorization_url, state = bs.authorization_url(authorization_base_url) + session['oauth_state'] = state + return redirect(authorization_url) + + +@application.route("/callback") +def callback(): + bs = OAuth2Session(client_id, state=session['oauth_state'], redirect_uri=redirect_uri, scope=[ + "openid profile", "https://buildingSMARTservices.onmicrosoft.com/api/read"]) + t = bs.fetch_token(token_url, client_secret=client_secret, + authorization_response=request.url, response_type="token") + BS_DISCOVERY_URL = ( + "https://buildingSMARTservices.b2clogin.com/buildingSMARTservices.onmicrosoft.com/b2c_1a_signupsignin_c/v2.0/.well-known/openid-configuration" + ) + session['oauth_token'] = t -@application.route('/', methods=['GET']) -def get_main(): - return render_template('index.html') + # Get claims thanks to openid + discovery_response = requests.get(BS_DISCOVERY_URL).json() + key = requests.get(discovery_response['jwks_uri']).content.decode("utf-8") + id_token = t['id_token'] + decoded = jwt.decode(id_token, key=key) + session['decoded'] = decoded + + with database.Session() as db_session: + user = db_session.query(database.user).filter(database.user.id == decoded["sub"]).all() + if len(user) == 0: + db_session.add(database.user(str(decoded["sub"]), + str(decoded["email"]), + str(decoded["family_name"]), + str(decoded["given_name"]), + str(decoded["name"]))) + db_session.commit() + + return redirect(url_for('index')) + + +@application.route("/logout") +@login_required +def logout(decoded): + session.clear() # Wipe out the user and the token cache from the session + return redirect( # Also need to log out from the Microsoft Identity platform + "https://buildingSMARTservices.b2clogin.com/buildingSMARTservices.onmicrosoft.com/b2c_1a_signupsignin_c/oauth2/v2.0/logout" + "?post_logout_redirect_uri=" + url_for("index", _external=True)) def process_upload(filewriter, callback_url=None): + id = utils.generate_id() d = utils.storage_dir_for_id(id) os.makedirs(d) - + filewriter(os.path.join(d, id+".ifc")) - + session = database.Session() - session.add(database.model(id, '')) + session.add(database.model(id, 'test')) session.commit() session.close() - + if DEVELOPMENT: + t = threading.Thread(target=lambda: worker.process(id, callback_url)) t.start() - else: q.enqueue(worker.process, id, callback_url) return id - def process_upload_multiple(files, callback_url=None): id = utils.generate_id() d = utils.storage_dir_for_id(id) os.makedirs(d) - + file_id = 0 session = database.Session() - m = database.model(id, '') + m = database.model(id, '') session.add(m) - + for file in files: fn = file.filename - filewriter = lambda fn: file.save(fn) + def filewriter(fn): return file.save(fn) filewriter(os.path.join(d, id+"_"+str(file_id)+".ifc")) file_id += 1 m.files.append(database.file(id, '')) - + session.commit() session.close() - + if DEVELOPMENT: t = threading.Thread(target=lambda: worker.process(id, callback_url)) - t.start() + + t.start() else: q.enqueue(worker.process, id, callback_url) return id +def process_upload_validation(files, validation_config, user_id, callback_url=None): + + ids = [] + filenames = [] + + with database.Session() as session: + for file in files: + fn = file.filename + filenames.append(fn) + def filewriter(fn): return file.save(fn) + id = utils.generate_id() + ids.append(id) + d = utils.storage_dir_for_id(id) + os.makedirs(d) + filewriter(os.path.join(d, id+".ifc")) + session.add(database.model(id, fn, user_id)) + session.commit() + + user = session.query(database.user).filter(database.user.id == user_id).all()[0] + + msg = f"{len(filenames)} file(s) were uploaded by user {user.name} ({user.email}): {(', ').join(filenames)}" + send_simple_message(msg) + + if DEVELOPMENT: + for id in ids: + t = threading.Thread(target=lambda: worker.process( + id, validation_config, callback_url)) + t.start() + else: + for id in ids: + q.enqueue(worker.process, id, validation_config, callback_url) + + return ids @application.route('/', methods=['POST']) -def put_main(): - """ - Upload model - --- - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - ifc: - type: string - format: binary - responses: - '200': - description: redirect - """ +@login_required +def put_main(decoded): + ids = [] - files = [] + + user_id = decoded["sub"] + for key, f in request.files.items(): + if key.startswith('file'): file = f - files.append(file) + files.append(file) + + validate(data=request.form, filepath='defs.yml') + + val_config = request.form.to_dict() + val_results = { + k + "log": 'n' for (k, v) in val_config.items() if k != "user"} + + validation_config = {} + validation_config["config"] = val_config + validation_config["results"] = val_results + + if VALIDATION: + ids = process_upload_validation(files, validation_config, user_id) + + elif VIEWER: + ids = process_upload_multiple(files) + + idstr = "" + for i in ids: + idstr += i + + if VALIDATION: + url = url_for('dashboard', user_id=user_id) - - id = process_upload_multiple(files) - url = url_for('check_viewer', id=id) + elif VIEWER: + url = url_for('check_viewer', id=idstr) if request.accept_mimetypes.accept_json: - return jsonify({"url":url}) - else: - return redirect(url) + return jsonify({"url": url}) @application.route('/p/', methods=['GET']) def check_viewer(id): if not utils.validate_id(id): abort(404) - return render_template('progress.html', id=id) - - + return render_template('progress.html', id=id) + +@application.route('/dashboard', methods=['GET']) +@login_required +def dashboard(decoded): + user_id = decoded['sub'] + # Retrieve user data + with database.Session() as session: + saved_models = session.query(database.model).filter(database.model.user_id == user_id).all() + saved_models.sort(key=lambda m: m.date, reverse=True) + saved_models = [model.serialize() for model in saved_models] + + return render_template('dashboard.html', user_id=user_id, saved_models=saved_models) + + +@application.route('/valprog/', methods=['GET']) +@login_required +def get_validation_progress(decoded, id): + if not utils.validate_id(id): + abort(404) + + all_ids = utils.unconcatenate_ids(id) + + model_progresses = [] + file_info = [] + with database.Session() as session: + for i in all_ids: + model = session.query(database.model).filter(database.model.code == i).all()[0] + + if model.user_id != decoded["sub"]: + abort(404) + + file_info.append({"number_of_geometries": model.number_of_geometries, + "number_of_properties": model.number_of_properties}) + + model_progresses.append(model.progress) + + return jsonify({"progress": model_progresses, "filename": model.filename, "file_info": file_info}) + + +@application.route('/update_info/', methods=['POST']) +@login_required +def update_info(decoded, code): + try: + validate(data=request.get_data(), filepath='update.yml') + with database.Session() as session: + model = session.query(database.model).filter(database.model.code == code).all()[0] + original_license = model.license + data = request.get_data() + decoded_data = json.loads(data) + + property = decoded_data["type"] + setattr(model, property, decoded_data["val"]) + + user = session.query(database.user).filter(database.user.id == model.user_id).all()[0] + + if decoded_data["type"] == "license": + send_simple_message( + f"User {user.name} ({user.email}) changed license of its file {model.filename} from {original_license} to {model.license}") + session.commit() + return jsonify( {"progress": data.decode("utf-8")}) + except: + return jsonify( {"progress": "an error happened"}) + + @application.route('/pp/', methods=['GET']) def get_progress(id): if not utils.validate_id(id): abort(404) session = database.Session() - model = session.query(database.model).filter(database.model.code == id).all()[0] + model = session.query(database.model).filter( + database.model.code == id).all()[0] session.close() return jsonify({"progress": model.progress}) @application.route('/log/.', methods=['GET']) def get_log(id, ext): - log_entry_type = namedtuple('log_entry_type', ("level", "message", "instance", "product")) - + log_entry_type = namedtuple( + 'log_entry_type', ("level", "message", "instance", "product")) + if ext not in {'html', 'json'}: abort(404) - + if not utils.validate_id(id): abort(404) logfn = os.path.join(utils.storage_dir_for_id(id), "log.json") if not os.path.exists(logfn): abort(404) - + if ext == 'html': log = [] for ln in open(logfn): l = ln.strip() if l: - log.append(json.loads(l, object_hook=lambda d: log_entry_type(*(d.get(k, '') for k in log_entry_type._fields)))) + log.append(json.loads(l, object_hook=lambda d: log_entry_type( + *(d.get(k, '') for k in log_entry_type._fields)))) return render_template('log.html', id=id, log=log) else: return send_file(logfn, mimetype='text/plain') @@ -225,12 +439,12 @@ def get_viewer(id): if not utils.validate_id(id): abort(404) d = utils.storage_dir_for_id(id) - + ifc_files = [os.path.join(d, name) for name in os.listdir(d) if os.path.isfile(os.path.join(d, name)) and name.endswith('.ifc')] - + if len(ifc_files) == 0: abort(404) - + failedfn = os.path.join(utils.storage_dir_for_id(id), "failed") if os.path.exists(failedfn): return render_template('error.html', id=id) @@ -239,9 +453,9 @@ def get_viewer(id): glbfn = ifc_fn.replace(".ifc", ".glb") if not os.path.exists(glbfn): abort(404) - + n_files = len(ifc_files) if "_" in ifc_files[0] else None - + return render_template( 'viewer.html', id=id, @@ -250,6 +464,78 @@ def get_viewer(id): ) +@application.route('/reslogs//') +@login_required +def log_results(decoded, i, ids): + all_ids = utils.unconcatenate_ids(ids) + with database.Session() as session: + model = session.query(database.model).filter( + database.model.code == all_ids[int(i)]).all()[0] + + response = {"results": {}, "time": None} + + response["results"]["syntaxlog"] = model.status_syntax + response["results"]["schemalog"] = model.status_schema + response["results"]["mvdlog"] = model.status_mvd + response["results"]["bsddlog"] = model.status_bsdd + response["results"]["idslog"] = model.status_ids + + response["time"] = model.serialize()['date'] + + return jsonify(response) + + +@application.route('/report2/') +@login_required +def view_report2(decoded, id): + with database.Session() as session: + session = database.Session() + + model = session.query(database.model).filter( + database.model.code == id).all()[0] + m = model.serialize() + + bsdd_validation_task = session.query(database.bsdd_validation_task).filter( + database.bsdd_validation_task.validated_file == model.id).all()[0] + + bsdd_results = session.query(database.bsdd_result).filter( + database.bsdd_result.task_id == bsdd_validation_task.id).all() + bsdd_results = [bsdd_result.serialize() for bsdd_result in bsdd_results] + + for bsdd_result in bsdd_results: + bsdd_result["bsdd_property_constraint"] = json.loads( + bsdd_result["bsdd_property_constraint"]) + + bsdd_validation_task = bsdd_validation_task.serialize() + instances = session.query(database.ifc_instance).filter( + database.ifc_instance.file == model.id).all() + instances = {instance.id: instance.serialize() for instance in instances} + + user_id = decoded['sub'] + return render_template("report_v2.html", + model=m, + bsdd_validation_task=bsdd_validation_task, + bsdd_results=bsdd_results, + instances=instances, + user_id=user_id) + + +@application.route('/download/', methods=['GET']) +@login_required +def download_model(decoded, id): + with database.Session() as session: + session = database.Session() + model = session.query(database.model).filter(database.model.id == id).all()[0] + code = model.code + path = utils.storage_file_for_id(code, "ifc") + + return send_file(path, attachment_filename=model.filename, as_attachment=True, conditional=True) + +@application.route('/delete/', methods=['GET']) +@login_required +def delete(decoded, id): + return "to implement" + @application.route('/m/', methods=['GET']) def get_model(fn): """ @@ -264,32 +550,32 @@ def get_model(fn): description: Model id and part extension example: BSESzzACOXGTedPLzNiNklHZjdJAxTGT.glb """ - - + id, ext = fn.split('.', 1) - + if not utils.validate_id(id): abort(404) - + if ext not in {"xml", "svg", "glb", "unoptimized.glb"}: abort(404) - - path = utils.storage_file_for_id(id, ext) + + path = utils.storage_file_for_id(id, ext) if not os.path.exists(path): abort(404) - + if os.path.exists(path + ".gz"): import mimetypes response = make_response( - send_file(path + ".gz", - mimetype=mimetypes.guess_type(fn, strict=False)[0]) + send_file(path + ".gz", + mimetype=mimetypes.guess_type(fn, strict=False)[0]) ) response.headers['Content-Encoding'] = 'gzip' return response else: return send_file(path) + """ # Create a file called routes.py with the following # example content to add application-specific routes diff --git a/application/static/dropzone.css b/application/static/dropzone.css deleted file mode 100644 index fe5cc953..00000000 --- a/application/static/dropzone.css +++ /dev/null @@ -1,397 +0,0 @@ -/* - * The MIT License - * Copyright (c) 2012 Matias Meno - */ - @-webkit-keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } - @-moz-keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } - @keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } - @-webkit-keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } - @-moz-keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } - @keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } - @-webkit-keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } - @-moz-keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } - @keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } - .dropzone, .dropzone * { - box-sizing: border-box; } - - .dropzone { - min-height: 150px; - border: 2px solid rgba(0, 0, 0, 0.3); - background: white; - padding: 20px 20px; } - .dropzone.dz-clickable { - cursor: pointer; } - .dropzone.dz-clickable * { - cursor: default; } - .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { - cursor: pointer; } - .dropzone.dz-started .dz-message { - display: none; } - .dropzone.dz-drag-hover { - border-style: solid; } - .dropzone.dz-drag-hover .dz-message { - opacity: 0.5; } - .dropzone .dz-message { - text-align: center; - margin: 2em 0; } - .dropzone .dz-message .dz-button { - background: none; - color: inherit; - border: none; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; } - .dropzone .dz-preview { - position: relative; - display: inline-block; - vertical-align: top; - margin: 16px; - min-height: 100px; } - .dropzone .dz-preview:hover { - z-index: 1000; } - .dropzone .dz-preview:hover .dz-details { - opacity: 1; } - .dropzone .dz-preview.dz-file-preview .dz-image { - border-radius: 20px; - background: #999; - background: linear-gradient(to bottom, #eee, #ddd); } - .dropzone .dz-preview.dz-file-preview .dz-details { - opacity: 1; } - .dropzone .dz-preview.dz-image-preview { - background: white; } - .dropzone .dz-preview.dz-image-preview .dz-details { - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -ms-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; } - .dropzone .dz-preview .dz-remove { - font-size: 14px; - text-align: center; - display: block; - cursor: pointer; - border: none; } - .dropzone .dz-preview .dz-remove:hover { - text-decoration: underline; } - .dropzone .dz-preview:hover .dz-details { - opacity: 1; } - .dropzone .dz-preview .dz-details { - z-index: 20; - position: absolute; - top: 0; - left: 0; - opacity: 0; - font-size: 13px; - min-width: 100%; - max-width: 100%; - padding: 2em 1em; - text-align: center; - color: rgba(0, 0, 0, 0.9); - line-height: 150%; } - .dropzone .dz-preview .dz-details .dz-size { - margin-bottom: 1em; - font-size: 16px; } - .dropzone .dz-preview .dz-details .dz-filename { - white-space: nowrap; } - .dropzone .dz-preview .dz-details .dz-filename:hover span { - border: 1px solid rgba(200, 200, 200, 0.8); - background-color: rgba(255, 255, 255, 0.8); } - .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { - overflow: hidden; - text-overflow: ellipsis; } - .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { - border: 1px solid transparent; } - .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { - background-color: rgba(255, 255, 255, 0.4); - padding: 0 0.4em; - border-radius: 3px; } - .dropzone .dz-preview:hover .dz-image img { - -webkit-transform: scale(1.05, 1.05); - -moz-transform: scale(1.05, 1.05); - -ms-transform: scale(1.05, 1.05); - -o-transform: scale(1.05, 1.05); - transform: scale(1.05, 1.05); - -webkit-filter: blur(8px); - filter: blur(8px); } - .dropzone .dz-preview .dz-image { - border-radius: 20px; - overflow: hidden; - width: 120px; - height: 120px; - position: relative; - display: block; - z-index: 10; } - .dropzone .dz-preview .dz-image img { - display: block; } - .dropzone .dz-preview.dz-success .dz-success-mark { - -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } - .dropzone .dz-preview.dz-error .dz-error-mark { - opacity: 1; - -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } - .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { - pointer-events: none; - opacity: 0; - z-index: 500; - position: absolute; - display: block; - top: 50%; - left: 50%; - margin-left: -27px; - margin-top: -27px; } - .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { - display: block; - width: 54px; - height: 54px; } - .dropzone .dz-preview.dz-processing .dz-progress { - opacity: 1; - -webkit-transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -ms-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - transition: all 0.2s linear; } - .dropzone .dz-preview.dz-complete .dz-progress { - opacity: 0; - -webkit-transition: opacity 0.4s ease-in; - -moz-transition: opacity 0.4s ease-in; - -ms-transition: opacity 0.4s ease-in; - -o-transition: opacity 0.4s ease-in; - transition: opacity 0.4s ease-in; } - .dropzone .dz-preview:not(.dz-processing) .dz-progress { - -webkit-animation: pulse 6s ease infinite; - -moz-animation: pulse 6s ease infinite; - -ms-animation: pulse 6s ease infinite; - -o-animation: pulse 6s ease infinite; - animation: pulse 6s ease infinite; } - .dropzone .dz-preview .dz-progress { - opacity: 1; - z-index: 1000; - pointer-events: none; - position: absolute; - height: 16px; - left: 50%; - top: 50%; - margin-top: -8px; - width: 80px; - margin-left: -40px; - background: rgba(255, 255, 255, 0.9); - -webkit-transform: scale(1); - border-radius: 8px; - overflow: hidden; } - .dropzone .dz-preview .dz-progress .dz-upload { - background: #333; - background: linear-gradient(to bottom, #666, #444); - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 0; - -webkit-transition: width 300ms ease-in-out; - -moz-transition: width 300ms ease-in-out; - -ms-transition: width 300ms ease-in-out; - -o-transition: width 300ms ease-in-out; - transition: width 300ms ease-in-out; } - .dropzone .dz-preview.dz-error .dz-error-message { - display: block; } - .dropzone .dz-preview.dz-error:hover .dz-error-message { - opacity: 1; - pointer-events: auto; } - .dropzone .dz-preview .dz-error-message { - pointer-events: none; - z-index: 1000; - position: absolute; - display: block; - display: none; - opacity: 0; - -webkit-transition: opacity 0.3s ease; - -moz-transition: opacity 0.3s ease; - -ms-transition: opacity 0.3s ease; - -o-transition: opacity 0.3s ease; - transition: opacity 0.3s ease; - border-radius: 8px; - font-size: 13px; - top: 130px; - left: -10px; - width: 140px; - background: #be2626; - background: linear-gradient(to bottom, #be2626, #a92222); - padding: 0.5em 1.2em; - color: white; } - .dropzone .dz-preview .dz-error-message:after { - content: ''; - position: absolute; - top: -6px; - left: 64px; - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #be2626; } - \ No newline at end of file diff --git a/application/static/icons/background.jpg b/application/static/icons/background.jpg new file mode 100644 index 00000000..4ae509c4 Binary files /dev/null and b/application/static/icons/background.jpg differ diff --git a/application/static/icons/background2.jpg b/application/static/icons/background2.jpg new file mode 100644 index 00000000..5ee11b9f Binary files /dev/null and b/application/static/icons/background2.jpg differ diff --git a/application/static/icons/bs.JPG b/application/static/icons/bs.JPG new file mode 100644 index 00000000..748be542 Binary files /dev/null and b/application/static/icons/bs.JPG differ diff --git a/application/static/icons/ifc.png b/application/static/icons/ifc.png new file mode 100644 index 00000000..783f7bea Binary files /dev/null and b/application/static/icons/ifc.png differ diff --git a/application/static/icons/invalid.png b/application/static/icons/invalid.png new file mode 100644 index 00000000..85440966 Binary files /dev/null and b/application/static/icons/invalid.png differ diff --git a/application/static/icons/not.png b/application/static/icons/not.png new file mode 100644 index 00000000..e0b7f22d Binary files /dev/null and b/application/static/icons/not.png differ diff --git a/application/static/icons/valid.png b/application/static/icons/valid.png new file mode 100644 index 00000000..4b565f63 Binary files /dev/null and b/application/static/icons/valid.png differ diff --git a/application/static/icons/warning.png b/application/static/icons/warning.png new file mode 100644 index 00000000..f789d6dc Binary files /dev/null and b/application/static/icons/warning.png differ diff --git a/application/static/index.css b/application/static/index.css new file mode 100644 index 00000000..f44be946 --- /dev/null +++ b/application/static/index.css @@ -0,0 +1,151 @@ +html, body{ + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +html, body, .full-screen { + height: 100%; +} + +.full-screen { + display: flex; + overflow: hidden; + flex-direction: column; +} + +/****** navigation ******/ + +.header { + display: flex; + align-items: center; + justify-content: space-between; + height: 90px; + width: 100%; + /* background-color:green */ +} + +.bsdd-logo { + height: 70px; +} + +.main { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + background-image: url('./icons/background2.jpg'); + background-repeat: no-repeat; + background-size: cover; + width: 100%; + flex: 1 1 auto; +} + + +/****** tabs ******/ +.tabs{ + display: flex; + align-items:center; + justify-content:space-around; + /* width:300px; + padding-left: 10px; + padding-right: 10px; */ + width: 320px; + background-color:#e7e7e7; + gap:3px; + font-family: Calibri; + height:30px; + border-top-right-radius:15px; + + +} + +.tab{ + border: none; +background: none; +width:50%; +font-size: 20px; + +/* height: 40px; */ +height: 100%; + +} + +#ifc_button{ + color:rgb(0, 157, 176); +} + +.tab:active{ + /* background:rgb(255, 255, 255); */ + border: none; + outline:0; +} +/* button:focus{ + background:rgb(255, 255, 255); + border: none; + outline:0; +} */ + +button:focus{ + outline:0; +} + +/* .tab:hover{ + background:bisque; +} */ + + + +/****** submit area ******/ + +.submit-area{ + + + display: flex; + flex-direction: column; + align-items: center; + background-color: rgb(255, 255, 255); + padding: 10px; + justify-content: space-evenly; + gap: 15px; + width: 300px; + height: 500px; + flex : 0 1 500px; +} + +#myDropzone { + display: flex; + flex-direction: column; + overflow: auto; + text-align: center; + height: 300px; + width: 80%; +} + +#config { + display: flex; + flex-direction: column; + justify-content: center; + text-align: left; + width: 80%; + background-color:rgba(0, 158, 176, 0.384); + /* background-color: rgb(181, 201, 255); */ + border-radius: 5px; + font-family: Calibri; +} + +.submit-button { + padding: 10px 20px; + border-radius: 5px; + width: 40%; + font-size: 15px; + /* border: none; */ +} + +a:visited { + color: inherit; + text-decoration: none; +} \ No newline at end of file diff --git a/application/static/main.css b/application/static/main.css index 10dbf67b..340e5dce 100644 --- a/application/static/main.css +++ b/application/static/main.css @@ -1,179 +1,179 @@ html, body, canvas { - display: block; - margin: 0; - padding: 0; - width: 100%; - height: 100%; + display: block; + margin: 0; + padding: 0; + width: 100%; + height: 100%; } form.upload { - padding: 32px; + padding: 32px; } .container { - padding: 10px; + padding: 10px; } h1.logo { - margin: 0; - font-weight: normal; + margin: 0; + font-weight: normal; } .progress { - width: 500px; - height: 24px; - border: solid 1px gray; - border-radius: 2px; + width: 500px; + height: 24px; + border: solid 1px gray; + border-radius: 2px; } .progress .percentage { - position: absolute; - width: 500px; - text-align: center; + position: absolute; + width: 500px; + text-align: center; } .progress .bar { - height: 24px; - border-right: solid 1px #aaa; - background: #eee; - width: 0px; + height: 24px; + border-right: solid 1px #aaa; + background: #eee; + width: 0px; } .bimsurfer-static-tree, .bimsurfer-metadata { - overflow: hidden; - overflow-y: scroll; + overflow: hidden; + overflow-y: scroll; } .bimsurfer-static-tree, .bimsurfer-static-tree * { - user-select: none; - -ms-user-select: none; - -moz-user-select: none; - -webkit-user-select: none; - z-index: 1; + user-select: none; + -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + z-index: 1; } .bimsurfer-static-tree div.item { - border-top: solid 1px #eee; - line-height: 12pt; + border-top: solid 1px #eee; + line-height: 12pt; } .bimsurfer-static-tree > div > div.item { - border: none; + border: none; } .bimsurfer-static-tree div.bimsurfer-tree-label { - margin-left: -100px; - padding-left: 100px; - width: 1000px; - cursor: pointer; + margin-left: -100px; + padding-left: 100px; + width: 1000px; + cursor: pointer; } .bimsurfer-static-tree .bimsurfer-tree-children-with-indent { - padding-left: 10px; + padding-left: 10px; } .bimsurfer-static-tree div.selected { - background: #6ae; - color: white; + background: #6ae; + color: white; } .bimsurfer-static-tree div.bimsurfer-tree-label:before { - content: "\21b3 "; +content: "\21b3 "; } .bimsurfer-static-tree div.bimsurfer-tree-eye:before { - content: "\1f441"; + content: "\1f441"; } - + .bimsurfer-tree-column { - display: inline-block; - overflow: hidden; + display: inline-block; + overflow: hidden; } div.bimsurfer-tree-eye-off { - opacity: 0.5; + opacity: 0.5; } .bimsurfer-metadata h3 { - font-weight: bold; - margin: 0; - padding: 10px 0 0 0; - width: 1000px; + font-weight: bold; + margin: 0; + padding: 10px 0 0 0; + width: 1000px; } .bimsurfer-metadata table { - width: 100%; + width: 100%; } .bimsurfer-metadata h3:before { - content: "\25b8 "; + content: "\25b8 "; } .bimsurfer-metadata th, .bimsurfer-metadata td { - width: 50%; - border-bottom: solid 1px #eee; - text-align: left; + width: 50%; + border-bottom: solid 1px #eee; + text-align: left; } .bimsurfer-metadata td:nth-child(1), .bimsurfer-metadata th:nth-child(1) { - width: 30%; + width: 30%; } .bimsurfer-metadata td:nth-child(2), .bimsurfer-metadata th:nth-child(2) { - width: 70%; + width: 70%; } .bimsurfer-metadata, .bimsurfer-metadata h3, .bimsurfer-static-tree { - font-size: 10pt; + font-size: 10pt; } .spinner { - position: fixed; - top: calc(50% - 64px); - left: calc(50% - 64px); - width: 128px; - height: 128px; - border: solid 4px gray; - border-bottom-color: white; - border-radius: 50%; - animation: spin 2s linear infinite; - z-index: 9999; +position: fixed; +top: calc(50% - 64px); +left: calc(50% - 64px); +width: 128px; +height: 128px; +border: solid 4px gray; +border-bottom-color: white; +border-radius: 50%; +animation: spin 2s linear infinite; +z-index: 9999; } @keyframes spin { - to { - transform: rotate(360deg); - } +to { + transform: rotate(360deg); +} } .log table { - border-collapse: collapse; - border-radius: 16px; - overflow: hidden; - width: 100%; - border: 0; + border-collapse: collapse; + border-radius: 16px; + overflow: hidden; + width: 100%; + border: 0; } .log td { - padding-left: 10px; + padding-left: 10px; } .log tr { - background: #f5f5f5; + background: #f5f5f5; } .log table thead tr { - height: 60px; - background: #ccc; +height: 60px; +background: #ccc; } .log table tbody tr { - height: 50px; +height: 50px; } .log th, .log td { - border: none; +border: none; } .log tr:nth-child(2n) { - background-color: #f0f0f0; + background-color: #f0f0f0; } diff --git a/application/static/navbar/BuildingSmart-items.png b/application/static/navbar/BuildingSmart-items.png new file mode 100644 index 00000000..a93f766e Binary files /dev/null and b/application/static/navbar/BuildingSmart-items.png differ diff --git a/application/static/navbar/BuildingSmart-login.png b/application/static/navbar/BuildingSmart-login.png new file mode 100644 index 00000000..92b0f9f4 Binary files /dev/null and b/application/static/navbar/BuildingSmart-login.png differ diff --git a/application/static/navbar/BuildingSmart-logo.png b/application/static/navbar/BuildingSmart-logo.png new file mode 100644 index 00000000..96aee1de Binary files /dev/null and b/application/static/navbar/BuildingSmart-logo.png differ diff --git a/application/static/navbar/buildingSMART_RGB_bSDD_service.png b/application/static/navbar/buildingSMART_RGB_bSDD_service.png new file mode 100644 index 00000000..7c6a5ebe Binary files /dev/null and b/application/static/navbar/buildingSMART_RGB_bSDD_service.png differ diff --git a/application/static/process.js b/application/static/process.js new file mode 100644 index 00000000..99043e35 --- /dev/null +++ b/application/static/process.js @@ -0,0 +1,227 @@ + + +function sendInfo(index = null) { + console.log(this) + var property = event.srcElement.id.split('_')[0]; + var modelCode = event.srcElement.id.split('_')[1]; + console.log(modelCode) + var data = { type: property, val: this.value, code: modelCode}; + + fetch("/update_info/" + modelCode, { + method: "POST", + body: JSON.stringify(data) + }).then(function (r) { return r.json(); }).then(function (r) { + console.log(r); + }) +} + +// Helper functions +function createLicenseInput(licensTypes, row, model){ + var licenseSelect = document.createElement("SELECT"); + + for (const license of licensTypes) { + var option = document.createElement("option"); + option.text = license; + licenseSelect.add(option); + } + + licenseSelect.id = "license_" + model.code; + licenseSelect.addEventListener("change", sendInfo); + licenseSelect.value = model.license + row.cells[toColumnComplete["license"]].appendChild(licenseSelect); + +} + +function createInput(type, row, model){ + var input = document.createElement("INPUT") + input.id = `${type}_${model.code}`; + input.addEventListener("change", sendInfo); + input.value = (type=="hours" ? model.hours : model.details); + row.cells[toColumnComplete[type]].appendChild(input); +} + +function replaceInCell(type, cell, modelId, replace=0){ + if(replace){ + cell.removeChild(cell.childNodes[0]); + } + cell.className = type; + var a = document.createElement('a'); + var text = (type=="download") ? "Download":"Delete"; + var linkText = document.createTextNode(text); + a.className = "dashboard_link" + a.appendChild(linkText); + a.title = type; + a.href = `/${type}/${modelId}`; + cell.appendChild(a); +} + + +var icons = { 'v': 'valid', 'w': 'warning', 'i': 'invalid', 'n': 'not' }; +function completeTable(i) { + var table = document.getElementById("saved_models"); + var row_index = idToRowIndex[modelIds[i]]; + var rows = table.rows; + + fetch("/reslogs/" + i + "/" + unsavedConcat).then(function (r) { return r.json(); }).then(function (r) { + ['syntaxlog', 'schemalog', 'mvdlog', 'bsddlog'].forEach((x, i) => { + var img = document.createElement("img"); + var icon = icons[r["results"][x]]; + rows[row_index].cells[i].className = icon; + img.src = "/static/icons/" + icons[r["results"][x]] + ".png"; + }); + + rows[row_index].cells[8].innerHTML = r["time"]; + rows[row_index].cells[8].className = "model_time"; + + }); + + + rows[row_index].cells[toColumnComplete["report"]].className = "model_report"; + var repText = document.createElement("a"); + repText.className = "dashboard_link"; + repText.id = "report"; + repText.innerHTML = "View report"; + repText.href = `/report2/${savedModels[i].code}`; + rows[row_index].cells[toColumnComplete["report"]].appendChild(repText); + + replaceInCell("download",rows[row_index].cells[toColumnComplete["download"]], savedModels[i].id, 1); + replaceInCell("delete",rows[row_index].cells[toColumnComplete["delete"]], savedModels[i].id, 1); +} + +var table = document.getElementById("saved_models"); +var nCols = Object.keys(toColumnComplete).length; +var unsavedConcat = ""; +var modelIds = []; +var codeToId = {}; +var idToRowIndex = {} + + +savedModels.forEach((model, i) => { + var rowIndex = i + 1; + var row = table.insertRow(rowIndex); + row.id = model.id; + + for (var col = 0; col < nCols; col++) { + row.insertCell(col); + } + + row.cells[toColumnComplete["file_format"]].className = "ifc"; + + row.cells[toColumnComplete["file_name"]].innerHTML = model.filename; + row.cells[toColumnComplete["file_name"]].className = "filename"; + + + var licensTypes = ["private", "CC", "MIT", "GPL", "LGPL"]; + + createLicenseInput(licensTypes, row, model); + createInput("hours", row, model); + createInput("details", row, model); + + if (model.progress == 100) { + var checks_type = ["syntax", "schema", "mvd", "bsdd", "ids"]; + var icons = { 'v': 'valid', 'w': 'warning', 'i': 'invalid', 'n': 'not' }; + for (var j = 0; j < checks_type.length; j++) { + var attr = "status_" + checks_type[j]; + var status_result = model[attr]; + var icon = icons[status_result]; + row.cells[toColumnComplete[checks_type[j]]].className = icon; + } + + var repText = document.createElement("a"); + repText.id = "report" + repText.innerHTML = "View report" + repText.href = `/report2/${model.code}`; + + row.cells[toColumnComplete["report"]].appendChild(repText) + row.cells[toColumnComplete["report"]].className = "model_report" + + row.cells[toColumnComplete["date"]].innerHTML = model.date + row.cells[toColumnComplete["date"]].className = "model_time" + + replaceInCell("download",row.cells[toColumnComplete["download"]], model.id); + replaceInCell("delete",row.cells[toColumnComplete["delete"]], model.id); + + row.cells[toColumnComplete["geoms"]].innerHTML = model.number_of_geometries; + row.cells[toColumnComplete["props"]].innerHTML = model.number_of_properties; + + } + + else { + console.log("unsaved"); + unsavedConcat += model.code; + modelIds.push(model.id); + + idToRowIndex[model.id] = rowIndex; + + row.cells[toColumnUncomplete["stop"]].innerHTML = "Stop"; + + const newDiv = document.createElement("div"); + newDiv.className = "progress" + const barDiv = document.createElement("div"); + barDiv.id = "bar" + model.id; + barDiv.className = "bar"; + newDiv.appendChild(barDiv) + row.cells[toColumnUncomplete["progress"]].appendChild(newDiv); + + row.cells[toColumnUncomplete["advancement"]].innerHTML = model.progress; + row.cells[toColumnUncomplete["advancement"]].id = "percentage" + model.id; + codeToId[model.code] = model.id; + + + } +}); + + +const registered = new Set(); +function poll(unsavedConcat) { + fetch("/valprog/" + unsavedConcat).then(function (r) { return r.json(); }).then(function (r) { + for (var i = 0; i < r.progress.length; i++) { + var str = unsavedConcat; + var modelCode = str.match(/.{1,32}/g) + var id = codeToId[modelCode[i]] + var percentage = document.getElementById("percentage" + id) + var bar = document.getElementById("bar" + id) + + var file_row = document.getElementById(id) + file_row.cells[toColumnUncomplete["geoms"]].innerHTML = r["file_info"][i]["number_of_geometries"] + file_row.cells[toColumnUncomplete["props"]].innerHTML = r["file_info"][i]["number_of_properties"] + + if (r.progress[i] === 100) { + + if (!registered.has(i)) { + + registered.add(i); + + bar.style.width = 100 * 2 + 'px'; + percentage.innerHTML = "100%" + completeTable(i); + + } + + } else { + var p = r.progress[i]; + if (p < 0) { + percentage.innerHTML = "in queue"; + p = 0 + } else { + percentage.innerHTML = p + "%"; + } + bar.style.width = p * 2 + 'px'; + + } + + } + + setTimeout(poll(unsavedConcat), 1000); + + }); + +} + + +if (unsavedConcat) { + console.log("/valprog/" + unsavedConcat); + poll(unsavedConcat); + +} + diff --git a/application/static/report.js b/application/static/report.js new file mode 100644 index 00000000..a37677e6 --- /dev/null +++ b/application/static/report.js @@ -0,0 +1,16 @@ +// var bsdd_validation_task = {{ bsdd_validation_task| tojson}}; +// var bsdd_results = {{ bsdd_results| tojson}}; +// var instances = {{ instances| tojson}}; + +console.log(bsdd_validation_task) +console.log(instances) +console.log(bsdd_results) + +for(var i=-0;i + + + + buildingSMART IFC Validation Service + + + + + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SyntaxSchemaMVDbSDDIDSFile format# geometries# propertiesLicenseProduction hoursAdditional details
+
+ + + + + + \ No newline at end of file diff --git a/application/templates/index.html b/application/templates/index.html index b54361a6..42fe5340 100644 --- a/application/templates/index.html +++ b/application/templates/index.html @@ -2,86 +2,212 @@ - IfcOpenShell viewer + buildingSMART IFC Validation Service - + + + - - {{ dropzone.load_css() }} - {{ dropzone.style('border: 2px dashed #0087F7; margin: 20px 10%; min-height: 400px;') }} - - - -

IfcOpenShell viewer

+ + + + + + +
+
+ + + + + +
+ +
+ +
+ + + + + + +
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + + +
+ + + + + + +
+ +
- {{ dropzone.create('put_main') }} - {{ dropzone.load_js() }} - - - + - + + \ No newline at end of file diff --git a/application/templates/report_v2.html b/application/templates/report_v2.html new file mode 100644 index 00000000..81799a34 --- /dev/null +++ b/application/templates/report_v2.html @@ -0,0 +1,340 @@ + + + + + + + + Report + + + + + + +
+ + + +
/ + + + + + + + {% set header_bg = "rgb(230, 242, 255)" %} +
+ +

Validation Report

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ General information about the file checked
Date{{model.date}}
File name{{model.filename}}
License{{model.license}}
File size{{model.size}}
Number of geometries{{model.number_of_geometries}}
Number of properties{{model.number_of_properties}}
IFC schema{{model.schema}}
Authoring application{{model.authoring_application }}
MVD(s){{model.mvd}}
+ + + + + + + + + {% set validation_symbols = + { + 'v': ['

', 'green'], + 'i': ['

','red'], + 'n':['

', 'black'], + 'w':['

','yellow'] + } + %} + + {% set check_types= + [ + "syntax", + "schema", + "mvd", + "bsdd", + "ids", + ] + %} + + {%for check_type in check_types%} + + + + + + {% endfor %} + + + +
+ Overall results of the validation
{{check_type}} + {{ validation_symbols[model['status_'+check_type]][0]|safe}} +
+ + + + + + + {%for bsdd_result in bsdd_results%} + + {% set constraints = bsdd_result['bsdd_property_constraint']%} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + + +
bSDD + consistency
+ IFC instance {{instances[bsdd_result['instance_id']]['global_id']}} + +
+ SPECIFICATIONS +
+ Classification URI + + {{bsdd_result['bsdd_classification_uri']}} +
+ IFC entity type + + {{bsdd_result['bsdd_type_constraint']}} +
+ Property set + + {{constraints['propertySet']}} +
+ Property name + + {{constraints['name']}} +
+ Data type + + {{constraints['dataType']}} +
+ Predefined value + + {{constraints['predefinedValue']}} +
+ RESULTS +
+ Entity type of the instance + + {{instances[bsdd_result['instance_id']]['ifc_type']}} +
+ Property set + + {{bsdd_result['ifc_property_set']}} +
+ Property name + + {{bsdd_result['ifc_property_name']}} +
+ Property value type + + {{bsdd_result['ifc_property_type']}} +
+ Value + + {{bsdd_result['ifc_property_value']}} +
 
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/application/templates/validation.html b/application/templates/validation.html new file mode 100644 index 00000000..6b0731f9 --- /dev/null +++ b/application/templates/validation.html @@ -0,0 +1,506 @@ + + + + + buildingSMART IFC Validation Service + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + {% for i in range(0, n_files) %} + {% set percentageid = "percentage" + i|string %} + {% set barid = "bar" + i|string %} + {% set fn = filenames[i]|string %} + + {% set format = filenames[i][-3::]|string %} + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + + + + {% for model in saved_models %} + + {% set icons = {'v':'valid', 'w':'warning', 'i':'invalid', 'n':'not'} %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
SyntaxSchemaMVDbSDDIDSFile format# geometries# propertiesLicenseProduction hoursAdditional details
{{fn}} + +
+
+
+ +
+
+
StopTestTest + {% if previous_file_input[i].license != None%} + + + + + {% else %} + + + + + {% endif %} + + + {% if previous_file_input[i].hours!= None %} + + + {% else %} + + {% endif %} + + {% if previous_file_input[i].details != None %} + + {% else %} + + {% endif %} + +
+ + + + + + + + + + {{model.filename}} + View report + {{model.date}}DownloadDelete{{model.number_of_geometries}}{{model.number_of_properties}} + + + + {% if model.hours != None %} + + {% else %} + + {% endif %} + + {% if model.details != None %} + + {% else %} + + {% endif %} +
+
+ + + + + + \ No newline at end of file diff --git a/application/update.yml b/application/update.yml new file mode 100644 index 00000000..24c5448c --- /dev/null +++ b/application/update.yml @@ -0,0 +1,24 @@ +Validation using JsonSchema for checking the update +--- +tags: + - update +parameters: + - name: body + in: formData + required: true + schema: + id: User + required: + - type + - val + - code + properties: + type: + type: string + default: "0" + val: + type: string + default: "0" + code: + type: string + default: "0" diff --git a/application/utils.py b/application/utils.py index 092d5739..4c5ef789 100644 --- a/application/utils.py +++ b/application/utils.py @@ -58,3 +58,6 @@ def validate_id(id): return len(set(id) - set(string.ascii_letters)) == 0 + +def unconcatenate_ids(id): + return [id[i:i+32] for i in range(0, len(id), 32)] \ No newline at end of file diff --git a/application/worker.py b/application/worker.py index 3da4db9b..7226bb16 100644 --- a/application/worker.py +++ b/application/worker.py @@ -34,6 +34,7 @@ import operator import shutil +import threading import requests on_windows = platform.system() == 'Windows' @@ -74,12 +75,115 @@ def __call__(self, directory, id, *args): self.sub_progress(100) -class ifc_validation_task(task): +class general_info_task(task): + est_time = 1 + + def execute(self, directory, id): + info_program = os.path.join(os.getcwd() + "/checks", "info.py") + subprocess.call([sys.executable, info_program, id + ".ifc", os.path.join(os.getcwd())], cwd=directory) + + +class syntax_validation_task(task): + est_time = 10 + + def execute(self, directory, id): + f = open(os.path.join(directory, "dresult_syntax.json"), "w") + check_program = os.path.join(os.getcwd() + "/checks/step-file-parser", "parse_file.py") + #subprocess.call([sys.executable, check_program, id + ".ifc", "--json"], cwd=directory, stdout=f) + proc = subprocess.Popen([sys.executable, check_program, id + ".ifc", "--json"], cwd=directory, stdout=subprocess.PIPE) + i = 0 + while True: + ch = proc.stdout.read(1) + + if not ch and proc.poll() is not None: + break + + if ch and ord(ch) == ord('.'): + i += 1 + self.sub_progress(i) + + + +class ifc_validation_task(task): + est_time = 10 + + def execute(self, directory, id): + f = open(os.path.join(directory, "dresult_schema.json"), "w") + check_program = os.path.join(os.getcwd() + "/checks", "validate.py") + proc = subprocess.Popen([sys.executable, check_program, id + ".ifc", "--json"], cwd=directory, stdout=subprocess.PIPE) + i = 0 + while True: + ch = proc.stdout.read(1) + + if not ch and proc.poll() is not None: + break + + if ch and ord(ch) == ord('.'): + i += 1 + self.sub_progress(i) + + +class mvd_validation_task(task): + est_time =10 + + + def execute(self, directory, id): + check_program = os.path.join(os.getcwd() + "/checks", "check_MVD.py") + outname = id +"_mvd.txt" + + with open(os.path.join(directory, outname), "w") as f: + subprocess.call([sys.executable, check_program, id + ".ifc"],cwd=directory,stdout=f) + +class bsdd_validation_task(task): + est_time = 10 + + def execute(self, directory, id): + + session = database.Session() + + model = session.query(database.model).filter(database.model.code == id).all()[0] + + validation_task = database.bsdd_validation_task(model.id) + + session.add(validation_task) + session.commit() + validation_task_id = str(validation_task.id) + session.close() + + check_program = os.path.join(os.getcwd() + "/checks", "check_bsdd_v2.py") + + proc = subprocess.Popen([sys.executable, check_program, "--input", id + ".ifc", "--task",validation_task_id], cwd=directory, stdout=subprocess.PIPE) + i = 0 + while True: + ch = proc.stdout.read(1) + + if not ch and proc.poll() is not None: + break + + if ch and ord(ch) == ord('.'): + i += 1 + self.sub_progress(i) + + +class ids_validation_task(task): + est_time = 10 def execute(self, directory, id): - with open(os.path.join(directory, "log.json"), "w") as f: - subprocess.call([sys.executable, "-m", "ifcopenshell.validate", id + ".ifc", "--json"], cwd=directory, stdout=f) + check_program = os.path.join(os.getcwd() + "/checks", "ids.py") + #todo allow series of ids specs to be processed + ids_files = [f for f in os.listdir(directory) if f.endswith(".xml")] + proc = subprocess.Popen([sys.executable, check_program, ids_files[0], id + ".ifc"], cwd=directory, stdout=subprocess.PIPE) + i = 0 + while True: + ch = proc.stdout.read(1) + + if not ch and proc.poll() is not None: + break + + if ch and ord(ch) == ord('.'): + i += 1 + self.sub_progress(i) class xml_generation_task(task): @@ -170,18 +274,66 @@ def execute(self, directory, id): self.sub_progress(i) -def do_process(id): +def do_process(id, validation_config, ids_spec): + d = utils.storage_dir_for_id(id) - input_files = [name for name in os.listdir(d) if os.path.isfile(os.path.join(d, name))] - - tasks = [ - ifc_validation_task, - xml_generation_task, - geometry_generation_task, - svg_generation_task, - glb_optimize_task, - gzip_task - ] + + + with open(os.path.join(d,'config.json'), 'w') as outfile: + json.dump(validation_config, outfile) + + if ids_spec: + ids_spec_storages = [] + n_ids_spec = int(len(ids_spec)/32) + ids_spec_storages = [] + b = 0 + j = 1 + a = 32 + + for n in range(n_ids_spec): + token = ids_spec[b:a] + ids_spec_storages.append(utils.storage_dir_for_id(token)) + # count += 1 + b = a + j+=1 + a = 32*j + + for ids_folder in ids_spec_storages: + for ids_file in os.listdir(ids_folder): + shutil.copy(os.path.join(ids_folder, ids_file), d ) + + input_files = [name for name in os.listdir(d) if os.path.isfile(os.path.join(d, name)) and os.path.join(d, name).endswith("ifc")] + + tasks = [general_info_task] + + + + for task, to_validate in validation_config["config"].items(): + + if int(to_validate): + if task == 'syntax' and to_validate: + tasks.append(syntax_validation_task) + elif task == 'schema' and to_validate: + tasks.append(ifc_validation_task) + elif task == 'mvd' and to_validate: + tasks.append(mvd_validation_task) + elif task == 'bsdd' and to_validate: + tasks.append(bsdd_validation_task) + elif task =='ids' and to_validate: + tasks.append(ids_validation_task) + + + # tasks = [ + # # syntax_validation_task, + # # ifc_validation_task, + # bsdd_validation_task, + # mvd_validation_task, + # # xml_generation_task, + # # geometry_generation_task, + # # svg_generation_task, + # # glb_optimize_task, + # # gzip_task + # ] tasks_on_aggregate = [] @@ -204,6 +356,7 @@ def execute(self, directory, id): print("Executing task 'print' on ", id, ' in ', directory, file=sys.stderr) """ + for fn in glob.glob("task_*.py"): mdl = importlib.import_module(fn.split('.')[0]) if getattr(mdl.task, 'aggregate_model', False): @@ -217,7 +370,7 @@ def execute(self, directory, id): elapsed = 0 set_progress(id, elapsed) - n_files = len([name for name in os.listdir(d) if os.path.isfile(os.path.join(d, name))]) + n_files = len([name for name in os.listdir(d) if os.path.isfile(os.path.join(d, name)) and os.path.join(d, name).endswith("ifc")]) total_est_time = \ sum(map(operator.attrgetter('est_time'), tasks)) * n_files + \ @@ -245,7 +398,8 @@ def run_task(t, args, aggregate_model=False): # to break out of nested loop else: continue break - + + for t in tasks_on_aggregate: run_task(t, [id, input_files], aggregate_model=True) @@ -253,13 +407,13 @@ def run_task(t, args, aggregate_model=False): set_progress(id, elapsed) -def process(id, callback_url): +def process(ids, validation_config, ids_spec = None , callback_url=None): + try: - do_process(id) + do_process(ids, validation_config, ids_spec) status = "success" except Exception as e: traceback.print_exc(file=sys.stdout) status = "failure" - if callback_url is not None: - r = requests.post(callback_url, data={"status": status, "id": id}) + r = requests.post(callback_url, data={"status": status, "id": ids}) diff --git a/docker-compose.yml b/docker-compose.yml index f2ec954c..bc2488d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,10 +5,12 @@ services: image: postgres:12.1-alpine expose: - "5432" + ports: + - "5432:5432" volumes: - ./docker-volumes/db:/var/lib/postgresql/data environment: - - POSTGRES_PASSWORD=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} redis: image: redis:5.0.7-alpine @@ -26,6 +28,15 @@ services: - MODEL_DIR=/data - REDIS_HOST=redis - POSTGRES_HOST=db + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - TESTING=${TESTING} + - CLIENT_ID=${CLIENT_ID} + - CLIENT_SECRET=${CLIENT_SECRET} + - MG_DOMAIN=${MG_DOMAIN} + - MG_KEY=${MG_KEY} + - MG_EMAIL=${MG_EMAIL} + - CRYPTOGRAPHY_DONT_BUILD_RUST=1 + expose: - "5000" depends_on: @@ -66,6 +77,7 @@ services: - REDIS_HOST=redis - POSTGRES_HOST=db - NUM_WORKERS=2 + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} depends_on: - redis - db diff --git a/nginx/app.conf b/nginx/app.conf index 77436117..4151a187 100644 --- a/nginx/app.conf +++ b/nginx/app.conf @@ -1,5 +1,4 @@ server { - listen 80 deferred; listen 443 ssl deferred; ssl_certificate /etc/letsencrypt/live/host/fullchain.pem; @@ -36,3 +35,10 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } } + +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$host$request_uri; +}