Replace existing code in src/opencdeserver with code from the "Kontroll OpenCDE API:s server" POC project on https://github.com/marwiss/Kontroll

This commit is contained in:
marwiss
2024-02-03 13:03:58 +01:00
committed by Dion Moult
parent 6d7a6eeda7
commit 31be899dc9
124 changed files with 9066 additions and 3388 deletions
+23
View File
@@ -0,0 +1,23 @@
FROM python:3.10-bullseye AS base
RUN apt-get update && apt-get install -y \
unzip \
&& rm -rf /var/lib/apt/lists/* \
COPY ./app/ifcopenshell/ifcopenshell.zip /ifcopenshell.zip
CMD ["unzip", "/ifcopenshell.zip", "-d", "/usr/local/lib/python3.10/site-packages/"]
WORKDIR /code/app
COPY ./app/requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
RUN pip install neo4j==5.9.0 uvicorn ifcopenshell Jinja2 python-multipart bcrypt --upgrade
# pip install: Install packages.
# --no-cache-dir: You can shrink the image size by disabling the cache.
# --upgrade: Upgrade packages.
# -r: use requirements file.
COPY ./app /code/app
FROM base AS server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", \
"--ssl-keyfile", "/etc/letsencrypt/live/kontroll.digital/privkey.pem", \
"--ssl-certfile", "/etc/letsencrypt/live/kontroll.digital/fullchain.pem"]
# If running behind a proxy like Nginx or Traefik add --proxy-headers.
# --ssl-keyfile: Path to keyfile.
# --ssl-certificat: Path to certificate.
View File
+566
View File
@@ -0,0 +1,566 @@
from uuid import UUID
from fastapi import APIRouter, Depends, UploadFile, HTTPException
from fastapi.responses import FileResponse
from security.secure import get_current_active_user
from models.bcf_request import *
from models.bcf_response import *
from models.request import *
from models.other import *
from repository.bcf import bcf_db
from api.logging import LoggingRoute
router = APIRouter(route_class=LoggingRoute)
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# BCF API (endpoints below only applies to BCF API versions 2.1 and 1.0)
# @router.get("/bcf/versions")
# def versions_get():
# return {"What": "Returns a list of all supported BCF API versions of the server."}
#
#
# @router.get("/bcf/3.0/auth")
# def auth_get(version: str):
# return {"What": "Obtaining Authentication Information."}
#
#
# @router.get("/bcf/3.0/current-user")
# def current_user_get(version: str):
# return {"What": "Get current user."}
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Projects
@router.get("/bcf/3.0/projects", tags=["projects_get"])
def projects_get(current_user: User = Depends(get_current_active_user)) -> List[ProjectGET]:
projects_response = bcf_db.get_projects(current_user)
bcf_db.debug(endpoint='projects_get',
request={},
response={count: value.dict() for count, value in enumerate(projects_response)})
return projects_response
@router.get("/bcf/3.0/projects/{project_id}", tags=["project_get"])
def project_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> ProjectGET:
project_response = bcf_db.get_project(project_id, current_user)
bcf_db.debug(endpoint='project_get',
request={'project_id': project_id},
response=project_response.dict())
return project_response
@router.put("/bcf/3.0/projects/{project_id}", tags=["project_put"], status_code=200)
def project_put(project_id: UUID, project_request: ProjectPUT,
current_user: User = Depends(get_current_active_user)) -> ProjectGET:
project_response = bcf_db.put_project(project_id, project_request, current_user)
bcf_db.debug(endpoint='project_put',
request={'project_id': project_id, 'project_request': project_request},
response=project_response.dict())
return project_response
@router.get("/bcf/3.0/projects/{project_id}/extensions", tags=["project_extensions_get"])
def project_extensions_get(project_id: UUID,
current_user: User = Depends(get_current_active_user)) -> ExtensionsGET:
extensions_response = bcf_db.get_project_extensions(project_id, current_user)
bcf_db.debug(endpoint='project_extensions_get',
request={'project_id': project_id},
response=extensions_response.dict())
return extensions_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Topics
@router.get("/bcf/3.0/projects/{project_id}/topics", tags=["topics_get"])
def topics_get(project_id: str,
current_user: User = Depends(get_current_active_user)) -> List[TopicGET]:
topics_response = bcf_db.get_topics(project_id, current_user)
bcf_db.debug(endpoint='topics_get',
request={'project_id': project_id},
response={count: value.dict() for count, value in enumerate(topics_response)})
return topics_response
@router.post("/bcf/3.0/projects/{project_id}/topics", tags=["topic_post"], status_code=201)
def topic_post(project_id: UUID, topic_request: TopicPOST,
current_user: User = Depends(get_current_active_user)) -> TopicGET:
topic_response = bcf_db.post_topic(project_id, topic_request, current_user)
if topic_response is None:
raise HTTPException(status_code=400, detail="Could not create topic.")
bcf_db.debug(endpoint='topic_post',
request={'project_id': project_id, 'topic_request': topic_request.dict()},
response=topic_response.dict())
return topic_response
# Implemented
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_get"])
def topic_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> TopicGET:
topic_response = bcf_db.get_topic(project_id, topic_id, current_user)
if topic_response is None:
raise HTTPException(status_code=404, detail="Item not found.")
bcf_db.debug(endpoint='topic_get',
request={'project_id': project_id, 'topic_id': topic_id},
response=topic_response.dict())
return topic_response
# Implemented
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_put"], status_code=200)
def topic_put(project_id: UUID, topic_id: UUID, topic_request: TopicPUT,
current_user: User = Depends(get_current_active_user)) -> TopicGET:
topic_response = bcf_db.put_topic(project_id, topic_id, topic_request, current_user)
bcf_db.debug(endpoint='topic_put',
request={'project_id': project_id, 'topic_id': topic_id, 'topic_request': topic_request.dict()},
response=topic_response.dict())
return topic_response
# Implemented
@router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}", tags=["topic_delete"], status_code=200)
def topic_delete(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> int:
topic_response = bcf_db.delete_topic(project_id, topic_id, current_user)
if topic_response is None:
raise HTTPException(status_code=404, detail="Item not found.")
bcf_db.debug(endpoint='topic_delete',
request={'project_id': project_id, 'topic_id': topic_id},
response={topic_response})
return topic_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# BIM snippets (not used by any client)
# Implemented
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/snippet", tags=["bim_snippet_get"])
def bim_snippet_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> BimSnippet:
bim_snippet_response = bcf_db.get_bim_snippet(project_id, topic_id, current_user)
if bim_snippet_response is None:
raise HTTPException(status_code=404, detail="Item not found.")
bcf_db.debug(endpoint='bim_snippet_get',
request={'project_id': project_id, 'topic_id': topic_id},
response=bim_snippet_response.dict())
return bim_snippet_response
# Implemented
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/snippet", tags=["bim_snippet_put"], status_code=200)
def bim_snippet_put(project_id: UUID, topic_id: UUID, snippet: BimSnippet,
current_user: User = Depends(get_current_active_user)) -> BimSnippet:
bim_snippet_response = bcf_db.put_bim_snippet(project_id, topic_id, snippet, current_user)
bcf_db.debug(endpoint='bim_snippet_put',
request={'project_id': project_id, 'topic_id': topic_id, 'snippet': snippet.dict()},
response=bim_snippet_response.dict())
return bim_snippet_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Files
@router.get("/bcf/3.0/projects/{project_id}/files_information", tags=["files_information_get"])
def files_information_get(project_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[ProjectFileInformation]:
files_information_response = bcf_db.get_files_information(project_id, current_user)
bcf_db.debug(endpoint='files_information_get',
request={'project_id': project_id},
response={count: value.dict() for count, value in enumerate(files_information_response)})
return files_information_response
# Implemented
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/files", tags=["files_get"])
def files_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[FileGET]:
files_response = bcf_db.get_files(project_id, topic_id, current_user)
bcf_db.debug(endpoint='files_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(files_response)})
return files_response
# request body file = FilePUT
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/files", tags=["files_put"], status_code=200)
def files_put(project_id: UUID, topic_id: UUID, files: List[FilePUT],
current_user: User = Depends(get_current_active_user)) -> List[FileGET]:
files_response = bcf_db.put_files(project_id, topic_id, files, current_user)
bcf_db.debug(endpoint='files_put',
request={'project_id': project_id,
'topic_id': topic_id,
'files': {count: value.dict() for count, value in enumerate(files)}},
response={count: value.dict() for count, value in enumerate(files_response)})
return files_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Comments
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments", tags=["comments_get"])
def comments_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[CommentGET]:
comments_response = bcf_db.get_comments(project_id, topic_id, current_user)
bcf_db.debug(endpoint='comments_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(comments_response)})
return comments_response
# request body comment = CommentPOST
@router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments", tags=["comment_post"], status_code=201)
def comment_post(project_id: UUID, topic_id: UUID, comment: CommentPOST,
current_user: User = Depends(get_current_active_user)) -> CommentGET:
comment_response = bcf_db.post_comment(project_id, topic_id, comment, current_user)
bcf_db.debug(endpoint='comment_post',
request={'project_id': project_id, 'topic_id': topic_id, 'comment': comment},
response=comment_response.dict())
return comment_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_get"])
def comment_get(project_id: UUID, topic_id: UUID, comment_id: UUID,
current_user: User = Depends(get_current_active_user)) -> CommentGET:
comment_response = bcf_db.get_comment(project_id, topic_id, comment_id, current_user)
bcf_db.debug(endpoint='comment_get',
request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id},
response=comment_response.dict())
return comment_response
# request body comment = CommentPUT
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}", tags=["comment_put"], status_code=200)
def comment_put(project_id: UUID, topic_id: UUID, comment_id: UUID, comment: CommentPUT,
current_user: User = Depends(get_current_active_user)) -> CommentGET:
comment_response = bcf_db.put_comment(project_id, topic_id, comment_id, comment, current_user)
bcf_db.debug(endpoint='comment_put',
request={'project_id': project_id, 'topic_id': topic_id, 'comment': comment},
response=comment_response.dict())
return comment_response
# Implemented
@router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}",
tags=["comment_delete"], status_code=200)
def comment_delete(project_id: UUID, topic_id: UUID, comment_id: UUID,
current_user: User = Depends(get_current_active_user)) -> int:
comment_response = bcf_db.delete_comment(project_id, topic_id, comment_id, current_user)
if comment_response is None:
raise HTTPException(status_code=404, detail="Item not found.")
bcf_db.debug(endpoint='comment_delete',
request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id},
response={comment_response})
return comment_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Viewpoints
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints", tags=["viewpoints_get"])
def viewpoints_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[ViewpointGET]:
viewpoints_response = bcf_db.get_viewpoints(project_id, topic_id, current_user)
bcf_db.debug(endpoint='viewpoints_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(viewpoints_response)})
return viewpoints_response
# request body viewpoint = viewpointPOST
@router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints", tags=["viewpoint_post"], status_code=201)
def viewpoint_post(project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST,
current_user: User = Depends(get_current_active_user)) -> ViewpointGET:
viewpoint_response = bcf_db.post_viewpoint(project_id, topic_id, viewpoint, current_user)
bcf_db.debug(endpoint='viewpoint_post',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint': viewpoint.dict()},
response=viewpoint_response.dict())
return viewpoint_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}", tags=["viewpoint_get"])
def viewpoint_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> ViewpointGET:
viewpoint_response = bcf_db.get_viewpoint(project_id, topic_id, viewpoint_id, current_user)
bcf_db.debug(endpoint='viewpoint_get',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response=viewpoint_response.dict())
return viewpoint_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/snapshot",
tags=["viewpoint_snapshot_get"])
async def viewpoint_snapshot_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> FileResponse:
viewpoint_snapshot_response = bcf_db.get_viewpoint_snapshot(project_id, topic_id, viewpoint_id, current_user)
bcf_db.debug(endpoint='viewpoint_snapshot_get',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response=viewpoint_snapshot_response)
snapshot_name = 'snapshot_' + str(viewpoint_id)
file_ending = '.' + viewpoint_snapshot_response.split('/', 2)[1]
snapshot_path = 'data/snapshots/' + snapshot_name + file_ending
snapshot_type = viewpoint_snapshot_response
return FileResponse(path=snapshot_path,
media_type=snapshot_type)
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/bitmaps/{bitmap_id}",
tags=["viewpoint_bitmap_get"])
async def viewpoint_bitmap_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID, bitmap_id: UUID,
current_user: User = Depends(get_current_active_user)) -> FileResponse:
viewpoint_bitmap_response = bcf_db.get_viewpoint_bitmap(project_id, topic_id, viewpoint_id, bitmap_id, current_user)
bcf_db.debug(endpoint='viewpoint_bitmap_get',
request={'project_id': project_id, 'topic_id': topic_id,
'viewpoint_id': viewpoint_id, 'bitmap_id': bitmap_id},
response=viewpoint_bitmap_response.dict())
bitmap_name = 'bitmap_' + str(viewpoint_id)
file_ending = '.' + viewpoint_bitmap_response['bitmap_type'].split('/', 2)[1]
bitmap_path = 'data/bitmaps/' + bitmap_name + file_ending
bitmap_type = viewpoint_bitmap_response['bitmap_type']
return FileResponse(path=bitmap_path,
media_type=bitmap_type)
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/coloring",
tags=["viewpoint_colored_components_get"])
def viewpoint_colored_components_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> ColoringGET:
viewpoint_colored_components_response = bcf_db.get_viewpoint_colored_components(project_id, topic_id, viewpoint_id, current_user)
bcf_db.debug(endpoint='viewpoint_colored_components_get',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response=viewpoint_colored_components_response.dict())
return viewpoint_colored_components_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/selection",
tags=["viewpoint_selected_components_get"])
def viewpoint_selected_components_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> SelectionGET:
viewpoint_selected_components_response = bcf_db.get_viewpoint_selected_components(project_id, topic_id, viewpoint_id, current_user)
bcf_db.debug(endpoint='viewpoint_selected_components_get',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response=viewpoint_selected_components_response.dict())
return viewpoint_selected_components_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}/visibility",
tags=["viewpoint_components_visibility_get"])
def viewpoint_components_visibility_get(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> VisibilityGET:
viewpoint_components_visibility_response = bcf_db.get_viewpoint_components_visibility(project_id, topic_id, viewpoint_id, current_user)
bcf_db.debug(endpoint='viewpoint_components_visibility_get',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response=viewpoint_components_visibility_response.dict())
return viewpoint_components_visibility_response
# Implemented
@router.delete("/bcf/3.0/projects/{project_id}/topics/{topic_id}/viewpoints/{viewpoint_id}",
tags=["viewpoint_delete"], status_code=200)
def viewpoint_delete(project_id: UUID, topic_id: UUID, viewpoint_id: UUID,
current_user: User = Depends(get_current_active_user)) -> int:
viewpoint_response = bcf_db.delete_viewpoint(project_id, topic_id, viewpoint_id, current_user)
if viewpoint_response is None:
raise HTTPException(status_code=404, detail="Item not found.")
bcf_db.debug(endpoint='viewpoint_delete',
request={'project_id': project_id, 'topic_id': topic_id, 'viewpoint_id': viewpoint_id},
response={viewpoint_response})
return viewpoint_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Related topics
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/related_topics", tags=["related_topics_get"])
def related_topics_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[RelatedTopicGET]:
related_topics_response = bcf_db.get_related_topics(project_id, topic_id, current_user)
bcf_db.debug(endpoint='related_topics_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(related_topics_response)})
return related_topics_response
# request body related_topic = RelatedTopicPUT
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/related_topics", tags=["related_topics_put"], status_code=200)
def related_topics_put(project_id: UUID, topic_id: UUID, related_topics: List[RelatedTopicPUT],
current_user: User = Depends(get_current_active_user)) -> List[RelatedTopicGET]:
related_topics_response = bcf_db.put_related_topics(project_id, topic_id, related_topics, current_user)
bcf_db.debug(endpoint='related_topics_put',
request={'project_id': project_id, 'topic_id': topic_id,
'related_topics': {count: value.dict() for count, value in enumerate(related_topics)}},
response={count: value.dict() for count, value in enumerate(related_topics_response)})
return related_topics_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Document references <- from topic
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references",
tags=["topic_document_references_get"])
def topic_document_references_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[DocumentReferenceGET]:
topic_document_references_response = bcf_db.get_topic_document_references(project_id, topic_id, current_user)
bcf_db.debug(endpoint='topic_document_references_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(topic_document_references_response)})
return topic_document_references_response
# request body document_reference = DocumentReferencePOST
@router.post("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references",
tags=["topic_document_references_post"],
status_code=201)
def topic_document_reference_post(project_id: UUID, topic_id: UUID, document_reference: DocumentReferencePOST,
current_user: User = Depends(get_current_active_user)) -> DocumentReferenceGET:
topic_document_references_response = bcf_db.post_topic_document_references(project_id,
topic_id,
document_reference,
current_user)
bcf_db.debug(endpoint='topic_document_references_post',
request={'project_id': project_id, 'topic_id': topic_id, 'document_reference': document_reference},
response={count: value.dict() for count, value in enumerate(topic_document_references_response)})
return topic_document_references_response
# request body document_reference = DocumentReferencePUT
@router.put("/bcf/3.0/projects/{project_id}/topics/{topic_id}/document_references/{reference_id}",
tags=["topic_document_references_put"],
status_code=200)
def topic_document_references_put(project_id: UUID, topic_id: UUID, reference_id: UUID,
document_reference: DocumentReferencePUT,
current_user: User = Depends(get_current_active_user)) -> DocumentReferenceGET:
topic_document_references_response = bcf_db.put_topic_document_references(project_id,
topic_id,
reference_id,
document_reference,
current_user)
bcf_db.debug(endpoint='topic_document_references_put',
request={'project_id': project_id,
'topic_id': topic_id,
'reference_id': reference_id,
'document_reference': document_reference.dict()},
response={count: value.dict() for count, value in enumerate(topic_document_references_response)})
return topic_document_references_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Documents project
@router.get("/bcf/3.0/projects/{project_id}/documents", tags=["documents_get"])
def documents_get(project_id: UUID, current_user: User = Depends(get_current_active_user)) -> List[DocumentGET]:
documents_response = bcf_db.get_documents(project_id, current_user)
bcf_db.debug(endpoint='documents_get',
request={'project_id': project_id},
response={count: value.dict() for count, value in enumerate(documents_response)})
return documents_response
# request body file = UploadFile
@router.post("/bcf/3.0/projects/{project_id}/documents", tags=["document_post"], status_code=201)
async def document_post(project_id: UUID, file: UploadFile,
current_user: User = Depends(get_current_active_user)) -> DocumentGET:
document_response = bcf_db.post_document(project_id, file, current_user)
bcf_db.debug(endpoint='document_post',
request={'project_id': project_id},
response=document_response.dict())
return document_response
@router.get("/bcf/3.0/projects/{project_id}/documents/{document_id}", tags=["document_get"])
def document_get(project_id: UUID, document_id: UUID,
current_user: User = Depends(get_current_active_user)) -> DocumentGET:
document_response = bcf_db.get_document(project_id, document_id, current_user)
bcf_db.debug(endpoint='document_get',
request={'project_id': project_id, 'document_id': document_id},
response=document_response.dict())
return document_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Topics events
# ...
@router.get("/bcf/3.0/projects/{project_id}/topics/events", tags=["topics_events_get"])
def topics_events_get(project_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[TopicEventGET]:
topic_events_response = bcf_db.get_topics_events(project_id, current_user)
bcf_db.debug(endpoint='topics_events_get',
request={'project_id': project_id},
response={count: value.dict() for count, value in enumerate(topic_events_response)})
return topic_events_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/events", tags=["topic_events_get"])
def topic_events_get(project_id: UUID, topic_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[TopicEventGET]:
topic_events_response = bcf_db.get_topic_events(project_id, topic_id, current_user)
bcf_db.debug(endpoint='topic_events_get',
request={'project_id': project_id, 'topic_id': topic_id},
response={count: value.dict() for count, value in enumerate(topic_events_response)})
return topic_events_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Comments events
@router.get("/bcf/3.0/projects/{project_id}/topics/comments/events", tags=["comments_events_get"])
def comments_events_get(project_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[CommentEventGET]:
comments_events_response = bcf_db.get_comments_events(project_id, current_user)
bcf_db.debug(endpoint='comments_events_get',
request={'project_id': project_id},
response={count: value.dict() for count, value in enumerate(comments_events_response)})
return comments_events_response
@router.get("/bcf/3.0/projects/{project_id}/topics/{topic_id}/comments/{comment_id}/events",
tags=["comment_events_get"])
def comment_events_get(project_id: UUID, topic_id: UUID, comment_id: UUID,
current_user: User = Depends(get_current_active_user)) -> List[CommentEventGET]:
comment_events_response = bcf_db.get_comment_events(project_id, topic_id, comment_id, current_user)
bcf_db.debug(endpoint='comment_events_get',
request={'project_id': project_id, 'topic_id': topic_id, 'comment_id': comment_id},
response={count: value.dict() for count, value in enumerate(comment_events_response)})
return comment_events_response
+762
View File
@@ -0,0 +1,762 @@
import collections
import os
import shutil
import traceback
import sys
from fastapi import HTTPException, status, APIRouter, Request, Depends
from fastapi import UploadFile, Form
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.encoders import jsonable_encoder
from typing import Union
from uuid import UUID, uuid4
from pydantic import ValidationError
from security.secure import get_current_active_user
from repository.documents import doc_db
from models.documents_request import *
from models.documents_response import *
from models.documents_common import *
from models.documents_other import *
from api.logging import LoggingRoute
router = APIRouter(route_class=LoggingRoute)
templates = Jinja2Templates(directory="templates")
################################################################
# DOCUMENTS API UPLOAD FLOW
################################################################
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Initiate the upload flow'
#
# description: 'The client will call this endpoint to initiate the flow of uploading documents to the CDE.'
#
# requestBody:
# description: 'The body of this request contains a list of files to upload and a callback URL to the client,
# which is used later in the flow to communicate from the CDE server back to the client via query parameters.'
#
# {
# "server_context": "7188a2be-6c4e-4e3b-b7e7-cd27f0d4ad67",
# "callback": {
# "url": "http://localhost:8080/cde-callback-example",
# "expires_in": 3600
# },
# "files": [
# {
# "file_name": "model.ifc",
# "session_file_id": "76ec9d91-0731-4405-b3c4-9bf945f9955b"
# }
# ]
# }
#
# The client is using the optional server_context in this example to provide a guid value
# which was obtained, from the CDE, in a previous document exchange. The server_context
# informs the CDE of the user's previous activity (e.g. a "project" or a directory on
# the CDE). The CDE can use the server_context to resume the document selection session
# from where the user has last left.
#
# description: 'The CDE returns a URL for the client to open in a local browser.
#
# {
# "upload_ui_url": "https://cde.example.com/document-upload?upload_session=7c41c859-c0c1-4914-ac6c-8fbd50fb8247",
# "expires_in": 60,
# "max_size_in_bytes": 1073741824
# }
#
# The user will then be presented with the CDE UI to enter document metadata'
#
# links /server-provided-path-upload-documents-url
#
# * Once the user has completed entering document metadata in the CDE UI,
# the CDE will append `?upload_documents_url=/server-provided-path-upload-documents-url`
# to the client's callback (see request) and provide it to the client via a browser redirect
#
# * If the user has cancelled the download the CDE server will append `?user_cancelled_selection=true`
# to the client's callback (see request) and provide it to the client via a browser redirect.
# implemented
@router.post("/documents/1.0/upload-documents", tags=[""])
def upload_documents_post(upload_documents: UploadDocuments,
current_user: User = Depends(get_current_active_user)) -> DocumentUploadSessionInitialization:
post_upload_documents_response = doc_db.post_upload_documents(upload_documents, current_user)
doc_db.debug(endpoint='upload_documents_post',
request={'upload_documents': upload_documents},
response=post_upload_documents_response.dict())
return post_upload_documents_response
# This goes to a website UI where the user can enter document metadata.
@router.get("/documents/1.0/document-upload", tags=[""], response_class=HTMLResponse)
def upload_documents_get(request: Request, upload_session: UUID):
data_for_upload_documents = doc_db.get_data_for_upload_documents(upload_session)
print('Data for site: ', data_for_upload_documents)
return templates.TemplateResponse(
'upload_files.html',
{'request': request,
'upload_session': upload_session,
'username': data_for_upload_documents.current_user.username,
'email': data_for_upload_documents.current_user.email,
'full_name': data_for_upload_documents.current_user.full_name,
'server_context': data_for_upload_documents.server_context,
'callback_url': data_for_upload_documents.callback.url,
'callback_expires_in': data_for_upload_documents.callback.expires_in,
'documents': data_for_upload_documents.documents,
'projects': data_for_upload_documents.projects})
@router.post("/documents/1.0/save-metadata-for-documents", tags=[""])
async def save_metadata_for_documents_post(request: Request) -> list:
form_data = await request.form()
form_data_json = jsonable_encoder(form_data)
print("Form values: ")
print(form_data_json)
documents = collections.defaultdict(dict)
names = ('session_file_id', 'document', 'title', 'version_number', 'filename')
for whole_form_key, value in form_data_json.items():
if whole_form_key.startswith(names):
start_form_key, document_id = whole_form_key.split("@", 1)
print('New field: ', start_form_key, ' for document id: ', document_id)
documents[document_id][start_form_key] = value
print("Documents: ")
print(documents)
username = form_data_json['username']
upload_session = form_data_json['upload_session']
server_context = form_data_json['server_context']
callback_url = form_data_json['callback_url']
callback_expires_in = form_data_json['callback_expires_in']
project = form_data_json['project']
documents_saved = list()
for key in documents:
try:
documents[key]['project'] = project
document = DocumentMetadata(**documents[key])
save_metadata_response = doc_db.save_metadata_for_documents(document, upload_session, username)
documents_saved.append(save_metadata_response)
except ValidationError as e:
print(e)
continue
doc_db.debug(endpoint='save_metadata_for_documents_post',
request={'documents': documents},
response={'response': documents_saved})
return documents_saved
# http://localhost:8080/cde-callback-example?upload_documents_url=
# https%3A%2F%2Fcde.example.com%2Fupload-instructions%3Fupload_session%3Dee56b8f3-8f93-4819-976e-46a45a5a996f
@router.post("/documents/1.0/upload-instructions", tags=[""])
def upload_instructions(session_id: str, server_context: str, upload_files: UploadFileDetails,
current_user: User = Depends(get_current_active_user)) -> DocumentsToUpload:
documents_to_upload_model = DocumentsToUpload()
documents_to_upload_model.server_context = server_context
documents_to_upload_model.documents_to_upload = list()
for upload_file in upload_files.files:
get_upload_instructions_response = doc_db.get_upload_instructions(session_id, server_context, upload_file, current_user)
doc_db.debug(endpoint='upload_instructions',
request={'session_id': session_id,
'server_context': server_context,
'document': upload_file},
response=get_upload_instructions_response.dict())
documents_to_upload_model.documents_to_upload.append(get_upload_instructions_response)
return documents_to_upload_model
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Upload a single file part'
#
# description: This endpoint allows the client to upload the content of a single file part.
# * The `/server-provided-path-upload-documents-url` operation specifies the list of part
# upload requests for each file
# * For each part, the URL, method (put or post), headers and request body are specified
# * The client must upload all parts before proceeding to complete the upload
# * Parts can be uploaded concurrently and in any order
#
# requestbody: 'The file content', application/octet-stream, type: string, format: binary
#
# responses: DocumentsToUpload
#
# links:
# /server-provided-path-document-upload-completion
# description: This operation should be called when all the parts have been successfully uploaded
#
# /server-provided-path-document-upload-cancellation
# description: This operation should be called to cancel the upload
@router.post("/documents/1.0/upload-part/{part_id}", tags=[""])
async def upload_part(part_id: str, request: Request,
current_user: User = Depends(get_current_active_user)):
# file_name = doc_db.safe_path(part_id)
file_name = part_id
# see if the user really has the part-node in the database, before receiving upload of this part
# and get the document_id of the part
document = doc_db.user_has_part(part_id, current_user)
request_body = await request.body()
if document:
# try to receive the uploaded part
try:
print('File contents: ', request_body)
# use document_id instead as dir_name
# dir_name = doc_db.safe_path(document.document_id)
dir_name = document.document_id
path = './data/document_parts/' + dir_name + '/'
if not os.path.exists(path):
os.makedirs(path)
with open(path + file_name, 'wb') as f:
f.write(request_body)
except Exception:
print('Error uploading file')
print(traceback.format_exc())
print('Error uploading file')
print(sys.exc_info()[2])
finally:
# We will write to the database, information about part successfully uploaded.
doc_db.mark_part_as_uploaded(part_id, current_user)
doc_db.debug(endpoint='upload-part',
request={'part_id': part_id},
response={'uploaded': True})
return {"message": f"Successfully uploaded part {file_name}"}
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Complete a file upload after all the parts have been successfully uploaded'
#
# description: The `/server-provided-path-upload-documents-url` operation specifies
# the upload completion URL for each file. The upload completion endpoint must be
# called by the client when the file content upload has completed successfully.
#
# responses: DocumentVersion
#
# links:
# /server-provided-path-document-upload-completion
# description: This operation should be called when all the parts have been successfully uploaded
#
# /server-provided-path-document-upload-cancellation
# description: This operation should be called to cancel the upload
@router.post("/documents/1.0/upload-completion", tags=[""])
def upload_completion(upload_session: str,
current_user: User = Depends(get_current_active_user)) -> Union[DocumentVersion, bool]:
# check if all parts really are marked as uploaded in database
# retrieve document_id, file_type, file_ending, and parts_id (in order)
if not doc_db.all_parts_uploaded(upload_session, current_user):
raise HTTPException(status_code=400, detail="All parts not uploaded.")
parts = doc_db.retrieve_uploaded_parts(upload_session, current_user)
print('Number of parts: ' + str(len(parts)))
document = doc_db.get_document_from_session(upload_session, current_user)
# check if all parts really are uploaded to document_id-dir
document_name = doc_db.safe_path(document.document_id)
path = './data/document_parts/' + document_name + '/'
for part in parts:
part = doc_db.safe_path(part)
print('Checking for part ' + part + ' in dir ' + path)
if not os.path.isfile(path + part):
print(part + ' is not in dir ' + path)
raise HTTPException(status_code=400, detail="All parts not in dir.")
else:
print(part + ' is in dir ' + path)
# merge parts to a new temporary document
temp_doc_path = path
temp_doc_file_name = path + document_name
if not os.path.exists(temp_doc_path):
os.makedirs(temp_doc_path)
new_doc_path = './data/documents/'
new_doc_path_name = new_doc_path + document.file_description.name
# Read parts and write to temp doc.
with open(temp_doc_file_name, 'ab') as temp_doc:
for part in parts:
part = doc_db.safe_path(part)
with open(temp_doc_path + part, 'rb') as part_doc:
temp_doc.write(part_doc.read())
# move document to new location in documents dir
os.rename(temp_doc_file_name, new_doc_path_name)
# remove temp parts
for part in parts:
os.remove(temp_doc_path + part)
# remove temp path
os.rmdir(temp_doc_path)
# clean database and create new document node under correct project
if doc_db.document_upload_finish(upload_session, current_user, document.project):
# get DocumentVersion
document = doc_db.get_document_version(document.document_id, document.version_index, current_user)
doc_db.debug(endpoint='upload_completion',
request={'upload_session': upload_session},
response=document.dict())
return document
else:
return False
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Cancel the upload of a single file'
#
# description: The `/server-provided-path-upload-documents-url` operation specifies the
# upload cancellation URL for each file. This endpoint must be called by the client when
# the file content upload has been cancelled.
#
# responses: 204
@router.post("/documents/1.0/upload-cancellation", tags=[""], status_code=status.HTTP_204_NO_CONTENT)
def upload_cancellation(upload_session: str, current_user: User = Depends(get_current_active_user)):
try:
# remove session and leaf nodes from database
document = doc_db.get_upload_cancellation(upload_session, current_user)
# clean temp dir
document_name = doc_db.safe_path(document.document_id)
path = './data/document_parts/' + document_name + '/'
shutil.rmtree(path)
except Exception as e:
print(e)
finally:
print('Upload cancellation complete.')
return
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Query for the latest versions of multiple documents'
#
# description: This endpoint can be called when querying for the latest versions for multiple
# documents. It's an aggregate endpoint that should make periodic polling of a collection
# of documents easier.
#
# parameters:
# in: header
# name: If-None-Match
# description: 'Clients may provide a previosuly received etag value to avoid receiving
# a response object if there were no changes since the last query'
# schema: type: string, format: etag_value
#
# responses
# description: 'A list of DocumentVersion objects, where each DocumentVersion
# represents the latest version for each given document on the server.'
#
# headers:
# ETag:
# schema: type: string, format: etag_value
#
# description: A unique identifier of the response that allows CDEs to save bandwidth and the
# clients to implement caching and avoid computation when the document versions haven't changed
# since the last query
#
# links
# /server-provided-path-document-versions
# These operations allows listing all the versions of a document
# that has been flagged to have a new version in the response.
#
@router.post("/documents/1.0/document-versions", tags=[""])
def document_versions_post(document_ids: List[UUID],
current_user: User = Depends(get_current_active_user)) -> List[DocumentVersion]:
document_versions = list()
for document_id in document_ids:
document_versions.append(doc_db.get_document_version(document_id, 1, current_user))
doc_db.debug(endpoint='document_versions_post',
request={document_ids},
response={document_versions})
return document_versions
################################################################
# DOWNLOAD FLOW
################################################################
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Initiate document selection and download flow'
#
# description: The client will call this endpoint to initiate the flow of selecting and downloading documents.
#
# requestBody: 'The body of this request contains a callback URL to the client, which is used later in the
# flow to communicate from the CDE server back to the client via query parameters.'
#
# {
# "server_context": "711c0744-0a92-489f-8ca1-13813aa2dee7",
# "callback": {
# "url": "http://localhost:8080/cde-callback-example",
# "expires_in": 3600
# },
# "supported_file_extensions": [
# ".ifc",
# ".ifczip"
# ]
# }
#
# responses: 'The CDE returns a URL for the client to open in a local browser.
# The user will then be presented with the CDE UI to select and download documents'
#
# {
# "select_documents_url": "https://cde.example.com/document-selection?selection_session=7c41c859-c0c1-4914-ac6c-8fbd50fb8247",
# "expires_in": 60
# }
#
# links /server-provided-path-selected-documents-url
#
# * Once the user has completed selecting documents in the CDE UI, the CDE will append
# `?selected_documents_url=/server-provided-path-selected-documents-url` to the client's
# callback (see request) and provide it to the client via a browser redirect.
#
# in this case we use a static url: selected-documents-url (but this is not safe)
#
# * IF the user has cancelled the download, the CDE server will append `?user_cancelled_selection=true`
# to the client's callback (see request) and provide it to the client via a browser redirect.
#
@router.post("/documents/1.0/select-documents", tags=[""])
def select_documents_post(select_documents: SelectDocuments,
current_user: User = Depends(get_current_active_user)) -> DocumentDiscoverySessionInitialization:
post_select_documents_response = doc_db.post_select_documents(select_documents, current_user)
doc_db.debug(endpoint='select_documents_post',
request={'select_documents': select_documents},
response=post_select_documents_response.dict())
print("Returns ", post_select_documents_response)
return post_select_documents_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Retrieve the user''s selection from the CDE'
#
# description: This endpoint returns the document selection. The client retrieves its URL
# via the callback URL from the CDE after the selection has finished from a query parameter
# named `selected_documents_url`. In case when the user cancels the selection on the CDE UI,
# this callback will still be called, but the `selected_documents_url` query parameter will
# not be present, instead the server will provide a query parameter called
# `user_cancelled_selection=true`.
#
# responses: SelectedDocuments
#
# links /server-provided-path-document-download
# Use the `document_version_download` operation to download the file contents
#
# links /server-provided-path-document-metadata
# Use the `document_version_metadata` URL operation to retrieve document metadata
# documents/1.0/document-selection?selection_session=7cf3dd70-c880-4fb1-9897-f60472959533
@router.get("/documents/1.0/document-selection", tags=[""], response_class=HTMLResponse)
def selected_documents_get(request: Request,
selection_session: UUID):
data_for_document_selection = doc_db.get_data_for_document_selection(selection_session)
return templates.TemplateResponse(
'select_files.html',
{'request': request,
'selection_session': selection_session,
'current_user': data_for_document_selection.current_user,
'server_context': data_for_document_selection.server_context,
'callback_url': data_for_document_selection.callback.url,
'callback_expires_in': data_for_document_selection.callback.expires_in,
'projects': data_for_document_selection.projects})
@router.post("/documents/1.0/mark-documents-as-selected", tags=[""])
async def mark_documents_as_selected_post(request: Request) -> DocumentsMarkedAsSelected:
form_data = await request.form()
form_data_json = jsonable_encoder(form_data)
print("Form values: ")
print(form_data_json)
documents = list()
for key, value in form_data_json.items():
if 'document_' in key:
document_id = key.split("ocument_", 1)[1]
documents.append(document_id)
print("Sends ", documents)
get_selected_response = doc_db.post_mark_documents_as_selected(documents, form_data_json['selection_session'])
doc_db.debug(endpoint='mark_some_documents_as_selected_post',
request={'documents': documents,
'form_data_json[selection_session]': form_data_json['selection_session']},
response=get_selected_response.dict())
return get_selected_response
@router.get("/documents/1.0/download-instructions", tags=[""])
def download_instructions(session_id: UUID, server_context: str,
current_user: User = Depends(get_current_active_user)) -> SelectedDocuments:
get_download_instructions_response = doc_db.get_download_instructions(session_id, server_context, current_user)
doc_db.debug(endpoint='download_instructions',
request={'session_id': session_id,
'server_context': server_context},
response=get_download_instructions_response.dict())
return get_download_instructions_response
# download links
@router.get("/documents/1.0/document/{document_id}/version/{version_index}", tags=[""])
def document_version(document_id: str, version_index: int,
current_user: User = Depends(get_current_active_user)) -> DocumentVersion:
# This endpoint returns the document version model itself.
get_document_version = doc_db.get_document_version(document_id, version_index, current_user)
doc_db.debug(endpoint='document_version',
request={'document_id': document_id,
'version_index': version_index},
response=get_document_version.dict())
return get_document_version
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Get document metadata for a single document'
#
# description: This endpoint returns the metadata for a single document. Its URL is retrieved
# from the links object in the initial document selection response, with the `document_version_metadata`
# property.
#
# responses: 'The file content', application/octet-stream, type: string, format: binary
#
#
# links /server-provided-path-document-download
# Use the `document_version_download` operation to download the file contents
#
# links /server-provided-path-document-metadata
# Use the `document_version_metadata` URL operation to retrieve document metadata
@router.get("/documents/1.0/document/{document_id}/version/{version_index}/metadata", tags=[""])
def document_version_metadata(document_id: str, version_index: int,
current_user: User = Depends(get_current_active_user)) -> DocumentMetadataEntries:
# The metadata for document versions is a list of key-value pairs
get_document_version_metadata_result = doc_db.get_document_version_metadata(document_id, version_index, current_user)
doc_db.debug(endpoint='document_version',
request={'document_id': document_id,
'version_index': version_index},
response=get_document_version_metadata_result.dict())
return get_document_version_metadata_result
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Download the document'
#
# description: Use this endpoint to download the document.
# Its URL is retrieved from the links object in the initial document selection response,
# with the `document_version_download` property.
#
# responses: 'The file content', application/octet-stream, type: string, format: binary
#
#
# links /server-provided-path-document-download
# Use the `document_version_download` operation to download the file contents
#
# links /server-provided-path-document-metadata
# Use the `document_version_metadata` URL operation to retrieve document metadata
@router.get("/documents/1.0/document/{document_id}/version/{version_index}/download", tags=[""])
def document_version_download(document_id: str, version_index: int,
current_user: User = Depends(get_current_active_user)) -> FileResponse:
# The url to download the binary content of this document version.
# May either directly return the result or redirect to a storage provider
keep_characters = (' ', '.', '_', '-')
document_id = "".join(c for c in document_id if c.isalnum() or c in keep_characters).rstrip()
file_location = './data/documents/' + document_id + '.ifc'
return FileResponse(file_location,
media_type='application/x-step',
filename='6dbd4d52-14db-11ee-be56-0242ac120002.ifc')
@router.get("/documents/1.0/document/{document_id}/version/{version_index}/versions", tags=[""])
def document_versions(document_id: str, version_index: int,
current_user: User = Depends(get_current_active_user)) -> DocumentVersions:
# This url returns a list of all document versions for the parent document.
# The client can use this URL to monitor for new document versions
get_document_versions_result = doc_db.get_document_versions(document_id, current_user)
doc_db.debug(endpoint='document_version',
request={'document_id': document_id},
response=get_document_versions_result.dict())
return get_document_versions_result
@router.get("/documents/1.0/document/{document_id}/version/{version_index}/details", tags=[""],
response_class=HTMLResponse)
def document_version_details(request: Request,
document_id: str,
version_index: int,
current_user: User = Depends(get_current_active_user)):
# This url returns a list of all document versions for the parent document.
# The client can use this URL to monitor for new document versions
details = doc_db.get_document_version(document_id, version_index, current_user)
doc_db.debug(endpoint='document_version',
request={'document_id': document_id,
'version_index': version_index},
response=details.dict())
return templates.TemplateResponse(
'document_details.html',
{'request': request,
'details': details})
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Get the versions of a single document'
#
# description: This endpoint returns the versions for a single document. Its URL is retrieved
# from the links object in the initial document selection response, with the `document_versions` property.
#
# responses: DocumentVersions
#
# links: /document-versions
# parameters:
# document_id: '$response.body#..document_id'
# description: > Use the `document_id` to query for updates for this document
# @router.get("/documents/1.0/document-versions", tags=[""])
# def document_versions_get(current_user: User = Depends(get_current_active_user)) -> DocumentVersions:
# xxx_response = doc_db.get_post_put_xxx(current_user)
# doc_db.debug(endpoint='xxx_get_post_put',
# request={},
# response={count: value.dict() for count, value in enumerate(xxx_response)})
# return xxx_response
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
# summary: 'Retrieve upload instructions after the user has entered document metadata on the CDE'
#
# description: This endpoint returns the document upload information. The client retrieves
# this URL via the callback URL from the server after the user's completion of the upload
# metadata entry from a query parameter named `upload_documents_url`. In case when the
# user cancels the upload on the CDE UI, this callback will still be called, but the
# `upload_documents_url` query parameter will not be present, instead the server will
# provide a query parameter called `user_cancelled_upload=true`.
#
# requestbody: description: 'The client sends the list of files to be uploaded with this
# request. The file sizes are then used by the server to break the file to multiple
# upload parts, as required.'
#
# responses: DocumentsToUpload
#
# links:
# /server-provided-path-document-upload-part
# description: This operation should be called for each part specified in this reponse
#
# /server-provided-path-document-upload-completion
# description: This operation should be called when all the parts have been successfully uploaded
#
# /server-provided-path-document-upload-cancellation
# description: This operation should be called to cancel the upload
################################################################
# CUSTOM UPLOAD FLOW, USING DOWNLOAD SESSION
################################################################
# TODO: Fortsätt här
# include selection session i result
@router.post("/documents/1.0/upload_file_to_project", tags=[""])
async def upload_documents_post(file: UploadFile, project: str = Form(...), selection_session: str = Form(...)) -> Document:
# Get the file size (in bytes)
file.file.seek(0, 2)
file_size = file.file.tell()
# Create document id for file
document_id = str(uuid4())
# Move cursor back to the beginning of the file
await file.seek(0)
# Find file name ending and create new storage file name
if file.filename.lower().endswith(tuple(file_types)):
file_ending = file.filename.split('.')[-1].lower()
name = document_id + '.' + file_ending
else:
file_ending = ''
name = document_id
# Get mime type and file type
mime_type = ''
file_type = ''
if hasattr(file_types, file_ending):
mime_type = file_types[file_ending]['mime_type']
file_type = file_types[file_ending]['file_type']
# Create document data
document_version_dict = {
'document_id': document_id,
'session_file_id': '',
'version_index': 1,
'version_number': '1',
'creation_date': doc_db.timestamp(),
'title': file.filename,
'original_file_name': file.filename,
'file_ending': file_ending,
'mime_type': mime_type,
'file_type': file_type,
'project': project,
'file_description': {
'name': name,
'size_in_bytes': file_size
}
}
document_version_model = Document(**document_version_dict)
# Save file to disc
upload_directory = './data/documents/'
destination_path = os.path.join(upload_directory, name)
with open(destination_path, 'wb') as buffer:
shutil.copyfileobj(file.file, buffer)
# create database record
inserted_document = doc_db.create_node_for_uploaded_file(selection_session, project, document_version_model)
print('Created node for document id: ' + str(inserted_document.document_id))
doc_db.create_ifc_graph_for_document(inserted_document.document_id)
# return document version of database record
return inserted_document
+299
View File
@@ -0,0 +1,299 @@
import os
from typing import Optional
from fastapi import APIRouter, Request, Form
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBasicCredentials, HTTPBasic
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from models.request import *
from repository.foundation import foundation_db
from security.secure import authenticate_user, get_current_active_user
from uuid import uuid4
from api.logging import LoggingRoute
from security.secure import get_secrets
secrets = get_secrets()
router = APIRouter(route_class=LoggingRoute)
http_basic = HTTPBasic()
oauth2_state = None
authorization_code = None
templates = Jinja2Templates(directory="templates")
clients = {
os.environ['KONTROLL_CLIENT_ID']:
{
'name': os.environ['KONTROLL_CLIENT_NAME'],
'secret': secrets['kontroll_client_secret']
}
}
# ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----
#
#
# Foundations API
# 2.1 Versions Service
# GET /foundation/versions
#
# The Versions service is used to discover the available OpenCDE APIs
# and where to find them. The api_base_url field specifies the base path for each API.
#
# To clarify, the versions service allows specifying api_base_url for the Foundation API. However, to ensure
# discoverability, the Versions service is always served from the /foundation/versions base path regardless of the
# api_base_url parameter value.
#
# PARAMETER TYPE DESCRIPTION REQUIRED
# api_id string Identifier of the API true
# version_id string Identifier of the version true
# detailed_version string URL of the specification on GitHub false
# api_base_url string Optional, fqURL, to allow servers to relocate the API false
@router.get("/foundation/versions", tags=["api_versions_get"])
def api_versions_get():
return {
"versions": [{
"api_id": "foundation",
"version_id": "1.0",
"detailed_version": "https://github.com/BuildingSMART/foundation-API/tree/release_1_0"
}, {
"api_id": "bcf",
"version_id": "3.0",
"detailed_version": "https://github.com/buildingSMART/BCF-API/tree/release_3_0",
"api_base_url": os.environ['KONTROLL_BASE_URL'] + "bcf/3.0"
}, {
"api_id": "documents",
"version_id": "1.0",
"detailed_version": "https://github.com/buildingSMART/documents-API/tree/release_1_0",
"api_base_url": os.environ['KONTROLL_BASE_URL'] + "documents/1.0"
}]
}
# 2.2.1 Obtaining authentication information
# GET /foundation/1.0/auth
#
# PARAMETER TYPE DESCRIPTION Required
# oauth2_auth_url string URL to authorization page false
# oauth2_token_url string URL for token requests false
# oauth2_dynamic_client_reg_url string URL for automated client registration false
# http_basic_supported boolean Indicates if Http Basic Authentication is supported false
# supported_oauth2_flows string[] array of supported OAuth2 flows true
#
# Requirement: oauth2_auth_url and oauth2_token_url must be present at the same time.
#
# "oauth2_dynamic_client_reg_url" is commented out since registration is not allowed.
# If properties are not present in the response, clients should assume that the functionality
# is not supported by the server.
@router.get("/foundation/1.0/auth",
tags=["foundation_auth_get"])
def authentication_get():
return_variable = {
"oauth2_auth_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/auth",
"oauth2_token_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/token",
# "oauth2_dynamic_client_reg_url": os.environ['KONTROLL_BASE_URL'] + "foundation/oauth2/reg",
"http_basic_supported": True,
"supported_oauth2_flows": [
"authorization_code_grant"
]
}
print(return_variable)
return return_variable
# Authentication url
# GET /foundation/oauth2/auth
#
# To initiate the workflow, the client sends the user to the "oauth2_auth_url" with the following parameters added:
#
# PARAMETER VALUE
# response_type code - as string literal
# client_id your client id
# state unique user defined value
# scope defines the scope of access
# redirect_uri The redirect_uri registered for your client. This parameter is optional for
# OAUTH servers that don't support multiple redirect URLs
#
# EXAMPLE
# GET https://api.kontroll.digital/foundation/oauth2/auth?
# response_type=code
# &client_id=<your_client_id>
# &state=<user_defined_string>
# &scope=<some_number>
# &redirect_uri=http://localhost:10445/Callback
@router.get("/foundation/oauth2/auth", response_class=HTMLResponse)
def authorization(request: Request,
response_type: str,
client_id: str,
state: str,
scope: str,
redirect_uri: str,
):
client_name = clients[client_id]['name']
print(f"Response type: {response_type}, "
f"Client_id: {client_id}, "
f"Client_name: {client_name}, "
f"State: {state}, "
f"Scope: {scope},"
f"Redirect_URI: {redirect_uri}."
)
# 3. Solibri sends the user to oauth2_auth_url with the following parameters:
# response_type=code, client_id=solibri_test_001, state=..., redirect_uri=uri, scope=...
# 4. The API will ask the user to sign in with username and password.
return templates.TemplateResponse(
"login.html",
{"request": request,
"response_type": response_type,
"client_id": client_id,
"client_name": client_name,
"state": state,
"scope": scope,
"redirect_uri": redirect_uri,
})
@router.get("/foundation/oauth2/code")
def code(username: str,
password: str,
response_type: str,
client_id,
client_name,
state: str,
redirect_uri: str,
scope: str = ''
):
global oauth2_state
oauth2_state = state
print('Username: ' + username + '. Password: ' + password)
user = authenticate_user(username, password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"}
)
# The user is signed in, now the main purpose of this function is to generate the authorization code.
# There are many ways to generate this code, in our app it is simple the word "test".
# Conventionally auth_code needs to be encrypted because it can contain credentials, scope
# and other request related details.
# This functions adds the authorization code to the user node in the graph.
# The authorization code node self-destructs after time delay SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS
global authorization_code
new_authorization_code = str(uuid4())
if foundation_db.create_authorization_code(username, new_authorization_code, scope):
authorization_code = new_authorization_code
print(f"Authorization code: {authorization_code}")
# 5. When the access code is returned, the server redirects the user to redirect_uri.
# http://localhost:10445/Callback&scope=test&code=xxx&state=xxx
# 6. The code-parameter will be appended to the redirect_uri as a query parameter
# this code-parameter is the access code
# 7. The state-parameter should also be appended to the redirect_uri as a query parameter
# 8. If the user denies client access redirect_uri will instead contain an error query parameter
return authorization_code
# /foundation/oauth2/reg is not in use, since registration is not allowed.
# Path to obtain token
# With the obtained authorization code,
# the client is able to request an access token from the server.
# The "oauth2_token_url" from the authentication resource
# is used to send token requests to, for example:
#
# Will return
# access_token string The issued OAuth2 token
# token_type string Always bearer
# expires_in integer The lifetime of the access token in seconds
# refresh_token string The issued OAuth2 refresh token, one-time-usable only
#
# The POST request should be done via HTTP Basic Authorization
# with your application client_id as the username
# and your client_secret as the password
#
# username: client_id
# password: client_secret
#
# example
# POST https://example.com/foundation/oauth2/token?grant_type=authorization_code&code=<your_authorization_code>
@router.post("/foundation/oauth2/token",
tags=["login_for_access_token_post"],
status_code=201)
def login_for_access_token(
grant_type: Optional[str] = Form(None),
refresh_token: Optional[str] = Form(None),
code: Optional[str] = Form(None),
credentials: HTTPBasicCredentials = Depends(http_basic)):
print('grant_type: ', grant_type)
print('refresh_token: ', refresh_token)
print('code: ', code)
print('credentials: ', credentials)
# The API should check that the credentials (client_id and client_secret) are correct
# credentials.username contains the client_id
# credentials.password contains the client_secret
if credentials.username not in clients or credentials.password != clients[credentials.username]['secret']:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect client_id or client_secret",
)
if grant_type == 'authorization_code':
# use authorization code,
# create access token and refresh token,
# delete authorization code
user_info = foundation_db.use_authorization_code(code)
elif grant_type == 'refresh_token':
# use refresh token to get access token
# delete old access token and old refresh token
# create new access token and a new refresh token
# return old access token and new refresh token
user_info = foundation_db.use_refresh_token(refresh_token)
user_info.token_type = "Bearer"
user_info.expires_in = int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS'])
print('user_info: ', user_info)
return user_info
# 3.1.1 Get current user
# GET /foundation/1.0/current-user
#
# Response body
# {
# "id": "Architect@example.com",
# "name": "John Doe"
# }
@router.get("/foundation/1.0/current-user", response_model=User, tags=["current_user_get"])
async def current_user_get(current_user: User = Depends(get_current_active_user)):
return current_user
+42
View File
@@ -0,0 +1,42 @@
from fastapi import FastAPI, APIRouter, Response, Request
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
from fastapi.routing import APIRoute
from starlette.types import Message
from typing import Callable, Dict, Any
import logging
import httpx
def log_info(req_body, res_body, route_url):
logging.info('request:' + route_url + ':' + str(req_body))
logging.info('response:' + route_url + ':' + str(res_body))
class LoggingRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
req_body = await request.body()
response = await original_route_handler(request)
route_url = str(request.url)
if isinstance(response, StreamingResponse):
res_body = b''
async for item in response.body_iterator:
res_body += item
task = BackgroundTask(log_info, req_body, res_body, route_url)
return Response(content=res_body, status_code=response.status_code,
headers=dict(response.headers), media_type=response.media_type, background=task)
else:
if hasattr(response, 'body'):
res_body = response.body
else:
res_body = {'no response': True}
response.background = BackgroundTask(log_info, req_body, res_body, route_url)
return response
return custom_route_handler
logging.basicConfig(filename='logs/info.log', level=logging.DEBUG)
+178
View File
@@ -0,0 +1,178 @@
import collections
import os
import traceback
import sys
from fastapi import APIRouter, Request, Depends
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.encoders import jsonable_encoder
from uuid import UUID
from pydantic import ValidationError
from security.secure import get_current_active_user
from repository.documents import doc_db
from models.documents_request import *
from models.documents_response import *
from models.documents_other import *
from api.logging import LoggingRoute
router = APIRouter(route_class=LoggingRoute)
templates = Jinja2Templates(directory="templates")
################################################################
# UPLOAD FLOW
################################################################
@router.post("/user/1.0/upload-documents", tags=[""])
def upload_documents_post(upload_documents: UploadDocuments,
current_user: User = Depends(get_current_active_user)) -> DocumentUploadSessionInitialization:
post_upload_documents_response = doc_db.post_upload_documents(upload_documents, current_user)
doc_db.debug(endpoint='upload_documents_post',
request={'upload_documents': upload_documents},
response=post_upload_documents_response.dict())
return post_upload_documents_response
# This goes to a website UI where the user can enter document metadata.
@router.get("/user/1.0/document-upload", tags=[""], response_class=HTMLResponse)
def upload_documents_get(request: Request, upload_session: UUID):
data_for_upload_documents = doc_db.get_data_for_upload_documents(upload_session)
print('Data for site: ', data_for_upload_documents)
return templates.TemplateResponse(
'upload_files.html',
{'request': request,
'upload_session': upload_session,
'username': data_for_upload_documents.current_user.username,
'email': data_for_upload_documents.current_user.email,
'full_name': data_for_upload_documents.current_user.full_name,
'server_context': data_for_upload_documents.server_context,
'callback_url': data_for_upload_documents.callback.url,
'callback_expires_in': data_for_upload_documents.callback.expires_in,
'documents': data_for_upload_documents.documents,
'projects': data_for_upload_documents.projects})
@router.post("/user/1.0/save-metadata-for-documents", tags=[""])
async def save_metadata_for_documents_post(request: Request) -> list:
form_data = await request.form()
form_data_json = jsonable_encoder(form_data)
print("Form values: ")
print(form_data_json)
documents = collections.defaultdict(dict)
names = ('session_file_id', 'document', 'title', 'version_number', 'filename')
for whole_form_key, value in form_data_json.items():
if whole_form_key.startswith(names):
start_form_key, document_id = whole_form_key.split("@", 1)
print('New field: ', start_form_key, ' for document id: ', document_id)
documents[document_id][start_form_key] = value
print("Documents: ")
print(documents)
username = form_data_json['username']
upload_session = form_data_json['upload_session']
server_context = form_data_json['server_context']
callback_url = form_data_json['callback_url']
callback_expires_in = form_data_json['callback_expires_in']
project = form_data_json['project']
documents_saved = list()
for key in documents:
try:
documents[key]['project'] = project
document = DocumentMetadata(**documents[key])
save_metadata_response = doc_db.save_metadata_for_documents(document, upload_session, username)
documents_saved.append(save_metadata_response)
except ValidationError as e:
print(e)
continue
doc_db.debug(endpoint='save_metadata_for_documents_post',
request={'documents': documents},
response={'response': documents_saved})
return documents_saved
# http://localhost:8080/cde-callback-example?upload_documents_url=
# https%3A%2F%2Fcde.example.com%2Fupload-instructions%3Fupload_session%3Dee56b8f3-8f93-4819-976e-46a45a5a996f
@router.post("/user/1.0/upload-part/{part_id}", tags=[""])
async def upload_part(part_id: str, request: Request,
current_user: User = Depends(get_current_active_user)):
# file_name = doc_db.safe_path(part_id)
file_name = part_id
# see if the user really has the part-node in the database, before receiving upload of this part
# and get the document_id of the part
document = doc_db.user_has_part(part_id, current_user)
request_body = await request.body()
if document:
# try to receive the uploaded part
try:
print('File contents: ', request_body)
# use document_id instead as dir_name
# dir_name = doc_db.safe_path(document.document_id)
dir_name = document.document_id
path = './data/document_parts/' + dir_name + '/'
if not os.path.exists(path):
os.makedirs(path)
with open(path + file_name, 'wb') as f:
f.write(request_body)
except Exception:
print('Error uploading file')
print(traceback.format_exc())
print('Error uploading file')
print(sys.exc_info()[2])
finally:
# We will write to the database, information about part successfully uploaded.
doc_db.mark_part_as_uploaded(part_id, current_user)
doc_db.debug(endpoint='upload-part',
request={'part_id': part_id},
response={'uploaded': True})
return {"message": f"Successfully uploaded part {file_name}"}
################################################################
# DOWNLOAD FLOW
################################################################
@router.get("/user/1.0/document/{document_id}/version/{version_index}/download", tags=[""])
def document_version_download(document_id: str, version_index: int,
current_user: User = Depends(get_current_active_user)) -> FileResponse:
# The url to download the binary content of this document version.
# May either directly return the result or redirect to a storage provider
keep_characters = (' ', '.', '_', '-')
document_id = "".join(c for c in document_id if c.isalnum() or c in keep_characters).rstrip()
file_location = './data/documents/' + document_id + '.ifc'
return FileResponse(file_location,
media_type='application/x-step',
filename='6dbd4d52-14db-11ee-be56-0242ac120002.ifc')
@@ -0,0 +1,258 @@
ISO-10303-21;
HEADER;FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0, QuantityTakeOffAddOnView, SpaceBoundary2ndLevelAddOnView]','Option [Elements to export: Selected elements only]','Option [Partial Structure Display: Entire Model]','Option [IFC Domain: All]','Option [Structural Function: All Elements]','Option [Convert Grid elements: On]','Option [Convert IFC Annotations and ARCHICAD 2D elements: Off]','Option [Convert 2D symbols of Doors and Windows: On]','Option [Export geometries that Participates in Collision Detection only: Off]','Option [Split complex elements: Off]','Option [Material Preservation: Explode where necessary]','Option [Elements in Solid Element Operations: Extruded/revolved]','Option [Elements with junctions: Extruded/revolved without junctions]','Option [IFC Site Location: At Project Origin]','Option [Curtain Wall export mode: Container Element]','Option [Railing export mode: Single Element]','Option [Stair export mode: Single Element]','Option [Properties To Export: All properties]','Option [Space containment: Off]','Option [Bounding Box: Off]','Option [Geometry to type objects: Off]','Option [Element Properties: Off]','Option [Building Material Properties: Off]','Option [Element Parameters: Off]','Option [Component Parameters: Off]','Option [IFC Base Quantities: On]','Option [Door Window Parameters: Off]','Option [IFC Space boundaries: On]','Option [ARCHICAD Zone Categories as IFC Space classification data: On]','Option [Element Classifications: Off]'),'2;1');
FILE_NAME('C:\\Users\\Yoga\\Desktop\\test.ifc','2023-09-04T21:58:54',('Arkitekten'),('Arkitektkontoret'),'The EXPRESS Data Manager Version 5.02.0100.09 : 26 Sep 2013','IFC file generated by GRAPHISOFT ARCHICAD 25.0.0 NOR FULL Windows version (IFC add-on version: 3002 NOR FULL).','Arkitekten');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1= IFCACTORROLE(.USERDEFINED.,'Ark:',$);
#2= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#6= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#9= IFCPERSON($,$,'Arkitekten',$,$,$,(#1),(#2,#6));
#15= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#17= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#20= IFCORGANIZATION($,'Arkitektkontoret',$,$,(#15,#17));
#27= IFCPERSONANDORGANIZATION(#9,#20,$);
#30= IFCORGANIZATION('GS','GRAPHISOFT','GRAPHISOFT',$,$);
#31= IFCAPPLICATION(#30,'25.0.0','ARCHICAD','IFC add-on version: 3002 NOR FULL');
#32= IFCOWNERHISTORY(#27,#31,$,.NOCHANGE.,$,$,$,1693857534);
#33= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#34= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#35= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#36= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#37= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.0174532925199),#36);
#38= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#39= IFCCONVERSIONBASEDUNIT(#38,.PLANEANGLEUNIT.,'DEGREE',#37);
#40= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
#41= IFCMEASUREWITHUNIT(IFCPOSITIVELENGTHMEASURE(0.000304617419787),#40);
#42= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#43= IFCCONVERSIONBASEDUNIT(#42,.SOLIDANGLEUNIT.,'SQUAREDEGREE',#41);
#44= IFCMONETARYUNIT(.NOK.);
#45= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#46= IFCMEASUREWITHUNIT(IFCTIMEMEASURE(31556926.),#45);
#47= IFCDIMENSIONALEXPONENTS(0,0,1,0,0,0,0);
#48= IFCCONVERSIONBASEDUNIT(#47,.TIMEUNIT.,'Year',#46);
#49= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#50= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
#51= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
#52= IFCSIUNIT(*,.ENERGYUNIT.,.MEGA.,.JOULE.);
#53= IFCDERIVEDUNIT((#56,#58,#60),.THERMALCONDUCTANCEUNIT.,$);
#55= IFCSIUNIT(*,.POWERUNIT.,$,.WATT.);
#56= IFCDERIVEDUNITELEMENT(#55,1);
#57= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#58= IFCDERIVEDUNITELEMENT(#57,-1);
#59= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#60= IFCDERIVEDUNITELEMENT(#59,-1);
#61= IFCDERIVEDUNIT((#64,#66,#68),.SPECIFICHEATCAPACITYUNIT.,$);
#63= IFCSIUNIT(*,.ENERGYUNIT.,$,.JOULE.);
#64= IFCDERIVEDUNITELEMENT(#63,1);
#65= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#66= IFCDERIVEDUNITELEMENT(#65,-1);
#67= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#68= IFCDERIVEDUNITELEMENT(#67,-1);
#69= IFCDERIVEDUNIT((#72,#74),.MASSDENSITYUNIT.,$);
#71= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#72= IFCDERIVEDUNITELEMENT(#71,1);
#73= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#74= IFCDERIVEDUNITELEMENT(#73,-1);
#75= IFCUNITASSIGNMENT((#33,#34,#35,#39,#43,#44,#48,#49,#50,#51,#52,#53,#61,#69));
#77= IFCDIRECTION((1.,0.,0.));
#81= IFCDIRECTION((0.,0.,1.));
#83= IFCCARTESIANPOINT((0.,0.,0.));
#85= IFCAXIS2PLACEMENT3D(#83,#81,#77);
#86= IFCDIRECTION((0.,1.));
#88= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,0.01,#85,#86);
#91= IFCPROJECT('1CGyzxdzHaJybf9L4wHy3R',#32,'Prosjektnavn','Prosjektbeskrivelse',$,$,'Fase',(#88),#75);
#98= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#100= IFCDIRECTION((1.,0.,0.));
#102= IFCDIRECTION((0.,0.,1.));
#104= IFCCARTESIANPOINT((0.,0.,0.));
#106= IFCAXIS2PLACEMENT3D(#104,#102,#100);
#107= IFCLOCALPLACEMENT($,#106);
#110= IFCSITE('01uPthe3Uixx0TRWar3Vt8',#32,'Eiendomsnavn',$,$,#107,$,$,.ELEMENT.,(59,55,52,428000),(10,42,27,972000),0.,'',#98);
#116= IFCRELAGGREGATES('2F2UAtyl4uWjrKORQfJcEW',#32,$,$,#91,(#110));
#122= IFCQUANTITYLENGTH('GrossPerimeter',$,$,0.);
#124= IFCQUANTITYAREA('GrossArea',$,$,0.);
#125= IFCELEMENTQUANTITY('17rUPIjEMxVrkRJ11moELl',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#122,#124));
#130= IFCRELDEFINESBYPROPERTIES('0l65N8yZCPuwGi0lRf7Opq',#32,$,$,(#110),#125);
#134= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#136= IFCDIRECTION((1.,0.,0.));
#138= IFCDIRECTION((0.,0.,1.));
#140= IFCCARTESIANPOINT((0.,0.,0.));
#142= IFCAXIS2PLACEMENT3D(#140,#138,#136);
#143= IFCLOCALPLACEMENT(#107,#142);
#145= IFCBUILDING('3AMeTOOFsdVLZuNV0$27s5',#32,'Bygningens navn','Bygningstype',$,#143,$,'Byggnummer',.ELEMENT.,$,$,#134);
#147= IFCRELAGGREGATES('0bntSkh7UDU_vAHXbaggwi',#32,$,$,#110,(#145));
#151= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#152= IFCELEMENTQUANTITY('3GDAgT3z640iFgZCkp8utf',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#151));
#154= IFCRELDEFINESBYPROPERTIES('3joCDTcjDP3x5zmHkLS3S0',#32,$,$,(#145),#152);
#158= IFCDIRECTION((1.,0.,0.));
#160= IFCDIRECTION((0.,0.,1.));
#162= IFCCARTESIANPOINT((0.,0.,1000.));
#164= IFCAXIS2PLACEMENT3D(#162,#160,#158);
#165= IFCLOCALPLACEMENT(#143,#164);
#167= IFCBUILDINGSTOREY('0St1hMhFUHZyrAECcby7I2',#32,'1. etasje',$,$,#165,$,$,.ELEMENT.,1000.);
#169= IFCRELAGGREGATES('0TRv38eowcVEYjXmnFlLK1',#32,$,$,#145,(#167));
#173= IFCQUANTITYLENGTH('NetHeight',$,$,2700.);
#174= IFCQUANTITYLENGTH('GrossHeight',$,$,2700.);
#175= IFCQUANTITYLENGTH('Height',$,$,2700.);
#176= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#177= IFCELEMENTQUANTITY('0t$Y2LddO4Hpr3Foz8Be1$',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#173,#174,#175,#176));
#179= IFCRELDEFINESBYPROPERTIES('0J4XjFI$kRh2sJoL52j_PV',#32,$,$,(#167),#177);
#183= IFCDIRECTION((1.,0.,0.));
#185= IFCDIRECTION((0.,0.,1.));
#187= IFCCARTESIANPOINT((-4423.22691561,10230.4382609,0.));
#189= IFCAXIS2PLACEMENT3D(#187,#185,#183);
#190= IFCLOCALPLACEMENT(#165,#189);
#192= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#194= IFCCARTESIANPOINT((0.,-300.));
#196= IFCCARTESIANPOINT((8599.33565217,-300.));
#198= IFCCARTESIANPOINT((8599.33565217,0.));
#200= IFCCARTESIANPOINT((0.,0.));
#202= IFCPOLYLINE((#194,#196,#198,#200,#194));
#204= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#202);
#205= IFCDIRECTION((1.,0.,0.));
#207= IFCDIRECTION((0.,0.,1.));
#209= IFCCARTESIANPOINT((0.,0.,0.));
#211= IFCAXIS2PLACEMENT3D(#209,#207,#205);
#212= IFCDIRECTION((0.,0.,1.));
#214= IFCEXTRUDEDAREASOLID(#204,#211,#212,2700.);
#215= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#214));
#222= IFCPRESENTATIONLAYERASSIGNMENT('230- Yttervegger (som generisk objekt eller for gruppering)',$,(#215,#232,#357,#366,#443,#452),$);
#225= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#226= IFCCARTESIANPOINT((0.,0.));
#228= IFCCARTESIANPOINT((8599.33565217,0.));
#230= IFCPOLYLINE((#226,#228));
#232= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#230));
#236= IFCPRODUCTDEFINITIONSHAPE($,$,(#215,#232));
#242= IFCWALLSTANDARDCASE('1Xezbq735AdBO_4DerG66G',#32,'YVT-A','',$,#190,#236,'61A3D974-1C31-4A9C-B63E-10DA35406190');
#257= IFCRELCONTAINEDINSPATIALSTRUCTURE('0PUNS2OuLwupG5o8M6iDor',#32,$,$,(#242,#373,#459),#167);
#261= IFCMATERIAL('Yttervegg');
#264= IFCCOLOURRGB($,1.,1.,1.);
#265= IFCSURFACESTYLERENDERING(#264,0.,IFCNORMALISEDRATIOMEASURE(0.3),$,$,$,IFCNORMALISEDRATIOMEASURE(0.69),$,.NOTDEFINED.);
#266= IFCSURFACESTYLE('Maling - 01 Blank',.BOTH.,(#265));
#268= IFCPRESENTATIONSTYLEASSIGNMENT((#266));
#270= IFCSTYLEDITEM($,(#268),$);
#272= IFCSTYLEDREPRESENTATION(#192,$,$,(#270));
#274= IFCMATERIALDEFINITIONREPRESENTATION($,$,(#272),#261);
#278= IFCMATERIALLAYER(#261,300.,.U.);
#280= IFCMATERIALLAYERSET((#278),'Yttervegg 300');
#283= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#284= IFCRELASSOCIATESMATERIAL('2msyZzaqHpLqjyOSMDJ6bC',#32,$,$,(#242),#283);
#287= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('3'),$);
#291= IFCPROPERTYSET('2axpxSTK16Nv0$I70apGdK',#32,'Reuse',$,(#287));
#293= IFCRELDEFINESBYPROPERTIES('36a5T_GLs751QxsAvh1iEB',#32,$,$,(#242),#291);
#297= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#298= IFCPROPERTYSET('04Pv5xI9$vG1lZdN7wMAEc',#32,'AC_Pset_RenovationAndPhasing',$,(#297));
#300= IFCRELDEFINESBYPROPERTIES('2AvYgZFQ$eywkTT_yzugG4',#32,$,$,(#242),#298);
#303= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#304= IFCPROPERTYSET('3vuFSkrI50S$hRePMCpJTW',#32,'Pset_WallCommon',$,(#303));
#306= IFCRELDEFINESBYPROPERTIES('3eVxLUBuO61JJXioN57x5f',#32,$,$,(#242),#304);
#309= IFCQUANTITYLENGTH('Length',$,$,8599.33565217);
#310= IFCQUANTITYLENGTH('Height',$,$,2700.);
#311= IFCQUANTITYLENGTH('Width',$,$,300.);
#312= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.57980069565);
#313= IFCQUANTITYAREA('NetFootprintArea',$,$,2.57980069565);
#314= IFCQUANTITYAREA('GrossSideArea',$,$,23.2182062609);
#315= IFCQUANTITYAREA('NetSideArea',$,$,23.2182062609);
#316= IFCQUANTITYVOLUME('GrossVolume',$,$,6.96546187826);
#317= IFCQUANTITYVOLUME('NetVolume',$,$,6.96546187826);
#318= IFCELEMENTQUANTITY('1uJa0ITKB1qrAAf8WtmaZ2',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#309,#310,#311,#312,#313,#314,#315,#316,#317));
#320= IFCRELDEFINESBYPROPERTIES('3eMBYSfULCCgJUrbtWuHgF',#32,$,$,(#242),#318);
#323= IFCWALLTYPE('2V6t5i6hK0w_M3VKaOTPG8',#32,'Yttervegg 300',$,$,$,$,'9F1B716C-1AB5-00EB-E583-7D4918759408',$,.NOTDEFINED.);
#325= IFCRELDEFINESBYTYPE('0GIkSqbN66cNEifXw6t8R_',#32,$,$,(#242,#373,#459),#323);
#328= IFCDIRECTION((1.,0.,0.));
#330= IFCDIRECTION((0.,0.,1.));
#332= IFCCARTESIANPOINT((-3058.25300257,12869.3878261,0.));
#334= IFCAXIS2PLACEMENT3D(#332,#330,#328);
#335= IFCLOCALPLACEMENT(#165,#334);
#336= IFCCARTESIANPOINT((0.,-300.));
#338= IFCCARTESIANPOINT((8508.3373913,-300.));
#340= IFCCARTESIANPOINT((8508.3373913,0.));
#342= IFCCARTESIANPOINT((0.,0.));
#344= IFCPOLYLINE((#336,#338,#340,#342,#336));
#346= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#344);
#347= IFCDIRECTION((1.,0.,0.));
#349= IFCDIRECTION((0.,0.,1.));
#351= IFCCARTESIANPOINT((0.,0.,0.));
#353= IFCAXIS2PLACEMENT3D(#351,#349,#347);
#354= IFCDIRECTION((0.,0.,1.));
#356= IFCEXTRUDEDAREASOLID(#346,#353,#354,2700.);
#357= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#356));
#360= IFCCARTESIANPOINT((0.,0.));
#362= IFCCARTESIANPOINT((8508.3373913,0.));
#364= IFCPOLYLINE((#360,#362));
#366= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#364));
#369= IFCPRODUCTDEFINITIONSHAPE($,$,(#357,#366));
#373= IFCWALLSTANDARDCASE('3eT96F_cnDsOufCEsotwtp',#32,'YVT-B','',$,#335,#369,'E874918F-FA6C-4DD9-8E29-30EDB2DFADF3');
#377= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#378= IFCRELASSOCIATESMATERIAL('1iVlD94vOmLa5OpxnkCqlJ',#32,$,$,(#373),#377);
#381= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('2'),$);
#382= IFCPROPERTYSET('3lu_1RsD2PskLfQWcCOLU3',#32,'Reuse',$,(#381));
#384= IFCRELDEFINESBYPROPERTIES('1PufL50Yxw16Mg0x9OekT1',#32,$,$,(#373),#382);
#388= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#389= IFCPROPERTYSET('0221Oat$Xdl8OooXaoH0Gp',#32,'AC_Pset_RenovationAndPhasing',$,(#388));
#391= IFCRELDEFINESBYPROPERTIES('3OX_1Z$WvLZFlLD1nCbhRr',#32,$,$,(#373),#389);
#394= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#395= IFCPROPERTYSET('1T4$VD7m_rVNvlN5DrtbUq',#32,'Pset_WallCommon',$,(#394));
#397= IFCRELDEFINESBYPROPERTIES('1v2J9jM6ihU_rOwjxcKYIn',#32,$,$,(#373),#395);
#400= IFCQUANTITYLENGTH('Length',$,$,8508.3373913);
#401= IFCQUANTITYLENGTH('Height',$,$,2700.);
#402= IFCQUANTITYLENGTH('Width',$,$,300.);
#403= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.55250121739);
#404= IFCQUANTITYAREA('NetFootprintArea',$,$,2.55250121739);
#405= IFCQUANTITYAREA('GrossSideArea',$,$,22.9725109565);
#406= IFCQUANTITYAREA('NetSideArea',$,$,22.9725109565);
#407= IFCQUANTITYVOLUME('GrossVolume',$,$,6.89175328696);
#408= IFCQUANTITYVOLUME('NetVolume',$,$,6.89175328696);
#409= IFCELEMENTQUANTITY('09pcUknANnc6EQrUWIhXdt',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#400,#401,#402,#403,#404,#405,#406,#407,#408));
#411= IFCRELDEFINESBYPROPERTIES('2zE052B_urXe2bvDBLgYv1',#32,$,$,(#373),#409);
#414= IFCDIRECTION((1.,0.,0.));
#416= IFCDIRECTION((0.,0.,1.));
#418= IFCCARTESIANPOINT((35.6878669942,15007.8469565,0.));
#420= IFCAXIS2PLACEMENT3D(#418,#416,#414);
#421= IFCLOCALPLACEMENT(#165,#420);
#422= IFCCARTESIANPOINT((0.,-300.));
#424= IFCCARTESIANPOINT((7234.36173913,-300.));
#426= IFCCARTESIANPOINT((7234.36173913,0.));
#428= IFCCARTESIANPOINT((0.,0.));
#430= IFCPOLYLINE((#422,#424,#426,#428,#422));
#432= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#430);
#433= IFCDIRECTION((1.,0.,0.));
#435= IFCDIRECTION((0.,0.,1.));
#437= IFCCARTESIANPOINT((0.,0.,0.));
#439= IFCAXIS2PLACEMENT3D(#437,#435,#433);
#440= IFCDIRECTION((0.,0.,1.));
#442= IFCEXTRUDEDAREASOLID(#432,#439,#440,2700.);
#443= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#442));
#446= IFCCARTESIANPOINT((0.,0.));
#448= IFCCARTESIANPOINT((7234.36173913,0.));
#450= IFCPOLYLINE((#446,#448));
#452= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#450));
#455= IFCPRODUCTDEFINITIONSHAPE($,$,(#443,#452));
#459= IFCWALLSTANDARDCASE('3k3LbXmTfDWODxdSuyCGYE',#32,'YVT-C','',$,#421,#455,'EE0D5961-C1DA-4D81-837B-9DCE3C31088E');
#463= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#464= IFCRELASSOCIATESMATERIAL('3_dZLENyJ5_qngksl2ie6r',#32,$,$,(#459),#463);
#467= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('1'),$);
#468= IFCPROPERTYSET('342IhYDnCDoqxc_Zm6g_fw',#32,'Reuse',$,(#467));
#470= IFCRELDEFINESBYPROPERTIES('0KYu05w72mseS$C1XTnSzM',#32,$,$,(#459),#468);
#474= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#475= IFCPROPERTYSET('2POedyNP84Nk7S4D$CmpS2',#32,'AC_Pset_RenovationAndPhasing',$,(#474));
#477= IFCRELDEFINESBYPROPERTIES('2oUR0$VhKaEPV9FW3eb0G3',#32,$,$,(#459),#475);
#480= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#481= IFCPROPERTYSET('2IgxHtV7Xo2x_LGYkyiDI6',#32,'Pset_WallCommon',$,(#480));
#483= IFCRELDEFINESBYPROPERTIES('2d$ext$iF9gxEs6l5zaPYh',#32,$,$,(#459),#481);
#486= IFCQUANTITYLENGTH('Length',$,$,7234.36173913);
#487= IFCQUANTITYLENGTH('Height',$,$,2700.);
#488= IFCQUANTITYLENGTH('Width',$,$,300.);
#489= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.17030852174);
#490= IFCQUANTITYAREA('NetFootprintArea',$,$,2.17030852174);
#491= IFCQUANTITYAREA('GrossSideArea',$,$,19.5327766957);
#492= IFCQUANTITYAREA('NetSideArea',$,$,19.5327766957);
#493= IFCQUANTITYVOLUME('GrossVolume',$,$,5.8598330087);
#494= IFCQUANTITYVOLUME('NetVolume',$,$,5.8598330087);
#495= IFCELEMENTQUANTITY('1LnynQ0O$3P0FeM4JPb_Ao',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#486,#487,#488,#489,#490,#491,#492,#493,#494));
#497= IFCRELDEFINESBYPROPERTIES('14UKMwdhaOGUYk9crv2_wD',#32,$,$,(#459),#495);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,258 @@
ISO-10303-21;
HEADER;FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0, QuantityTakeOffAddOnView, SpaceBoundary2ndLevelAddOnView]','Option [Elements to export: Selected elements only]','Option [Partial Structure Display: Entire Model]','Option [IFC Domain: All]','Option [Structural Function: All Elements]','Option [Convert Grid elements: On]','Option [Convert IFC Annotations and ARCHICAD 2D elements: Off]','Option [Convert 2D symbols of Doors and Windows: On]','Option [Export geometries that Participates in Collision Detection only: Off]','Option [Split complex elements: Off]','Option [Material Preservation: Explode where necessary]','Option [Elements in Solid Element Operations: Extruded/revolved]','Option [Elements with junctions: Extruded/revolved without junctions]','Option [IFC Site Location: At Project Origin]','Option [Curtain Wall export mode: Container Element]','Option [Railing export mode: Single Element]','Option [Stair export mode: Single Element]','Option [Properties To Export: All properties]','Option [Space containment: Off]','Option [Bounding Box: Off]','Option [Geometry to type objects: Off]','Option [Element Properties: Off]','Option [Building Material Properties: Off]','Option [Element Parameters: Off]','Option [Component Parameters: Off]','Option [IFC Base Quantities: On]','Option [Door Window Parameters: Off]','Option [IFC Space boundaries: On]','Option [ARCHICAD Zone Categories as IFC Space classification data: On]','Option [Element Classifications: Off]'),'2;1');
FILE_NAME('C:\\Users\\Yoga\\Desktop\\test.ifc','2023-09-04T21:58:54',('Arkitekten'),('Arkitektkontoret'),'The EXPRESS Data Manager Version 5.02.0100.09 : 26 Sep 2013','IFC file generated by GRAPHISOFT ARCHICAD 25.0.0 NOR FULL Windows version (IFC add-on version: 3002 NOR FULL).','Arkitekten');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1= IFCACTORROLE(.USERDEFINED.,'Ark:',$);
#2= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#6= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#9= IFCPERSON($,$,'Arkitekten',$,$,$,(#1),(#2,#6));
#15= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#17= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#20= IFCORGANIZATION($,'Arkitektkontoret',$,$,(#15,#17));
#27= IFCPERSONANDORGANIZATION(#9,#20,$);
#30= IFCORGANIZATION('GS','GRAPHISOFT','GRAPHISOFT',$,$);
#31= IFCAPPLICATION(#30,'25.0.0','ARCHICAD','IFC add-on version: 3002 NOR FULL');
#32= IFCOWNERHISTORY(#27,#31,$,.NOCHANGE.,$,$,$,1693857534);
#33= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#34= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#35= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#36= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#37= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.0174532925199),#36);
#38= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#39= IFCCONVERSIONBASEDUNIT(#38,.PLANEANGLEUNIT.,'DEGREE',#37);
#40= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
#41= IFCMEASUREWITHUNIT(IFCPOSITIVELENGTHMEASURE(0.000304617419787),#40);
#42= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#43= IFCCONVERSIONBASEDUNIT(#42,.SOLIDANGLEUNIT.,'SQUAREDEGREE',#41);
#44= IFCMONETARYUNIT(.NOK.);
#45= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#46= IFCMEASUREWITHUNIT(IFCTIMEMEASURE(31556926.),#45);
#47= IFCDIMENSIONALEXPONENTS(0,0,1,0,0,0,0);
#48= IFCCONVERSIONBASEDUNIT(#47,.TIMEUNIT.,'Year',#46);
#49= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#50= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
#51= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
#52= IFCSIUNIT(*,.ENERGYUNIT.,.MEGA.,.JOULE.);
#53= IFCDERIVEDUNIT((#56,#58,#60),.THERMALCONDUCTANCEUNIT.,$);
#55= IFCSIUNIT(*,.POWERUNIT.,$,.WATT.);
#56= IFCDERIVEDUNITELEMENT(#55,1);
#57= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#58= IFCDERIVEDUNITELEMENT(#57,-1);
#59= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#60= IFCDERIVEDUNITELEMENT(#59,-1);
#61= IFCDERIVEDUNIT((#64,#66,#68),.SPECIFICHEATCAPACITYUNIT.,$);
#63= IFCSIUNIT(*,.ENERGYUNIT.,$,.JOULE.);
#64= IFCDERIVEDUNITELEMENT(#63,1);
#65= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#66= IFCDERIVEDUNITELEMENT(#65,-1);
#67= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#68= IFCDERIVEDUNITELEMENT(#67,-1);
#69= IFCDERIVEDUNIT((#72,#74),.MASSDENSITYUNIT.,$);
#71= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#72= IFCDERIVEDUNITELEMENT(#71,1);
#73= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#74= IFCDERIVEDUNITELEMENT(#73,-1);
#75= IFCUNITASSIGNMENT((#33,#34,#35,#39,#43,#44,#48,#49,#50,#51,#52,#53,#61,#69));
#77= IFCDIRECTION((1.,0.,0.));
#81= IFCDIRECTION((0.,0.,1.));
#83= IFCCARTESIANPOINT((0.,0.,0.));
#85= IFCAXIS2PLACEMENT3D(#83,#81,#77);
#86= IFCDIRECTION((0.,1.));
#88= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,0.01,#85,#86);
#91= IFCPROJECT('1CGyzxdzHaJybf9L4wHy3R',#32,'Prosjektnavn','Prosjektbeskrivelse',$,$,'Fase',(#88),#75);
#98= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#100= IFCDIRECTION((1.,0.,0.));
#102= IFCDIRECTION((0.,0.,1.));
#104= IFCCARTESIANPOINT((0.,0.,0.));
#106= IFCAXIS2PLACEMENT3D(#104,#102,#100);
#107= IFCLOCALPLACEMENT($,#106);
#110= IFCSITE('01uPthe3Uixx0TRWar3Vt8',#32,'Eiendomsnavn',$,$,#107,$,$,.ELEMENT.,(59,55,52,428000),(10,42,27,972000),0.,'',#98);
#116= IFCRELAGGREGATES('2F2UAtyl4uWjrKORQfJcEW',#32,$,$,#91,(#110));
#122= IFCQUANTITYLENGTH('GrossPerimeter',$,$,0.);
#124= IFCQUANTITYAREA('GrossArea',$,$,0.);
#125= IFCELEMENTQUANTITY('17rUPIjEMxVrkRJ11moELl',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#122,#124));
#130= IFCRELDEFINESBYPROPERTIES('0l65N8yZCPuwGi0lRf7Opq',#32,$,$,(#110),#125);
#134= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#136= IFCDIRECTION((1.,0.,0.));
#138= IFCDIRECTION((0.,0.,1.));
#140= IFCCARTESIANPOINT((0.,0.,0.));
#142= IFCAXIS2PLACEMENT3D(#140,#138,#136);
#143= IFCLOCALPLACEMENT(#107,#142);
#145= IFCBUILDING('3AMeTOOFsdVLZuNV0$27s5',#32,'Bygningens navn','Bygningstype',$,#143,$,'Byggnummer',.ELEMENT.,$,$,#134);
#147= IFCRELAGGREGATES('0bntSkh7UDU_vAHXbaggwi',#32,$,$,#110,(#145));
#151= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#152= IFCELEMENTQUANTITY('3GDAgT3z640iFgZCkp8utf',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#151));
#154= IFCRELDEFINESBYPROPERTIES('3joCDTcjDP3x5zmHkLS3S0',#32,$,$,(#145),#152);
#158= IFCDIRECTION((1.,0.,0.));
#160= IFCDIRECTION((0.,0.,1.));
#162= IFCCARTESIANPOINT((0.,0.,1000.));
#164= IFCAXIS2PLACEMENT3D(#162,#160,#158);
#165= IFCLOCALPLACEMENT(#143,#164);
#167= IFCBUILDINGSTOREY('0St1hMhFUHZyrAECcby7I2',#32,'1. etasje',$,$,#165,$,$,.ELEMENT.,1000.);
#169= IFCRELAGGREGATES('0TRv38eowcVEYjXmnFlLK1',#32,$,$,#145,(#167));
#173= IFCQUANTITYLENGTH('NetHeight',$,$,2700.);
#174= IFCQUANTITYLENGTH('GrossHeight',$,$,2700.);
#175= IFCQUANTITYLENGTH('Height',$,$,2700.);
#176= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#177= IFCELEMENTQUANTITY('0t$Y2LddO4Hpr3Foz8Be1$',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#173,#174,#175,#176));
#179= IFCRELDEFINESBYPROPERTIES('0J4XjFI$kRh2sJoL52j_PV',#32,$,$,(#167),#177);
#183= IFCDIRECTION((1.,0.,0.));
#185= IFCDIRECTION((0.,0.,1.));
#187= IFCCARTESIANPOINT((-4423.22691561,10230.4382609,0.));
#189= IFCAXIS2PLACEMENT3D(#187,#185,#183);
#190= IFCLOCALPLACEMENT(#165,#189);
#192= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#194= IFCCARTESIANPOINT((0.,-300.));
#196= IFCCARTESIANPOINT((8599.33565217,-300.));
#198= IFCCARTESIANPOINT((8599.33565217,0.));
#200= IFCCARTESIANPOINT((0.,0.));
#202= IFCPOLYLINE((#194,#196,#198,#200,#194));
#204= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#202);
#205= IFCDIRECTION((1.,0.,0.));
#207= IFCDIRECTION((0.,0.,1.));
#209= IFCCARTESIANPOINT((0.,0.,0.));
#211= IFCAXIS2PLACEMENT3D(#209,#207,#205);
#212= IFCDIRECTION((0.,0.,1.));
#214= IFCEXTRUDEDAREASOLID(#204,#211,#212,2700.);
#215= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#214));
#222= IFCPRESENTATIONLAYERASSIGNMENT('230- Yttervegger (som generisk objekt eller for gruppering)',$,(#215,#232,#357,#366,#443,#452),$);
#225= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#226= IFCCARTESIANPOINT((0.,0.));
#228= IFCCARTESIANPOINT((8599.33565217,0.));
#230= IFCPOLYLINE((#226,#228));
#232= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#230));
#236= IFCPRODUCTDEFINITIONSHAPE($,$,(#215,#232));
#242= IFCWALLSTANDARDCASE('1Xezbq735AdBO_4DerG66G',#32,'YVT-A','',$,#190,#236,'61A3D974-1C31-4A9C-B63E-10DA35406190');
#257= IFCRELCONTAINEDINSPATIALSTRUCTURE('0PUNS2OuLwupG5o8M6iDor',#32,$,$,(#242,#373,#459),#167);
#261= IFCMATERIAL('Yttervegg');
#264= IFCCOLOURRGB($,1.,1.,1.);
#265= IFCSURFACESTYLERENDERING(#264,0.,IFCNORMALISEDRATIOMEASURE(0.3),$,$,$,IFCNORMALISEDRATIOMEASURE(0.69),$,.NOTDEFINED.);
#266= IFCSURFACESTYLE('Maling - 01 Blank',.BOTH.,(#265));
#268= IFCPRESENTATIONSTYLEASSIGNMENT((#266));
#270= IFCSTYLEDITEM($,(#268),$);
#272= IFCSTYLEDREPRESENTATION(#192,$,$,(#270));
#274= IFCMATERIALDEFINITIONREPRESENTATION($,$,(#272),#261);
#278= IFCMATERIALLAYER(#261,300.,.U.);
#280= IFCMATERIALLAYERSET((#278),'Yttervegg 300');
#283= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#284= IFCRELASSOCIATESMATERIAL('2msyZzaqHpLqjyOSMDJ6bC',#32,$,$,(#242),#283);
#287= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('3'),$);
#291= IFCPROPERTYSET('2axpxSTK16Nv0$I70apGdK',#32,'Reuse',$,(#287));
#293= IFCRELDEFINESBYPROPERTIES('36a5T_GLs751QxsAvh1iEB',#32,$,$,(#242),#291);
#297= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#298= IFCPROPERTYSET('04Pv5xI9$vG1lZdN7wMAEc',#32,'AC_Pset_RenovationAndPhasing',$,(#297));
#300= IFCRELDEFINESBYPROPERTIES('2AvYgZFQ$eywkTT_yzugG4',#32,$,$,(#242),#298);
#303= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#304= IFCPROPERTYSET('3vuFSkrI50S$hRePMCpJTW',#32,'Pset_WallCommon',$,(#303));
#306= IFCRELDEFINESBYPROPERTIES('3eVxLUBuO61JJXioN57x5f',#32,$,$,(#242),#304);
#309= IFCQUANTITYLENGTH('Length',$,$,8599.33565217);
#310= IFCQUANTITYLENGTH('Height',$,$,2700.);
#311= IFCQUANTITYLENGTH('Width',$,$,300.);
#312= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.57980069565);
#313= IFCQUANTITYAREA('NetFootprintArea',$,$,2.57980069565);
#314= IFCQUANTITYAREA('GrossSideArea',$,$,23.2182062609);
#315= IFCQUANTITYAREA('NetSideArea',$,$,23.2182062609);
#316= IFCQUANTITYVOLUME('GrossVolume',$,$,6.96546187826);
#317= IFCQUANTITYVOLUME('NetVolume',$,$,6.96546187826);
#318= IFCELEMENTQUANTITY('1uJa0ITKB1qrAAf8WtmaZ2',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#309,#310,#311,#312,#313,#314,#315,#316,#317));
#320= IFCRELDEFINESBYPROPERTIES('3eMBYSfULCCgJUrbtWuHgF',#32,$,$,(#242),#318);
#323= IFCWALLTYPE('2V6t5i6hK0w_M3VKaOTPG8',#32,'Yttervegg 300',$,$,$,$,'9F1B716C-1AB5-00EB-E583-7D4918759408',$,.NOTDEFINED.);
#325= IFCRELDEFINESBYTYPE('0GIkSqbN66cNEifXw6t8R_',#32,$,$,(#242,#373,#459),#323);
#328= IFCDIRECTION((1.,0.,0.));
#330= IFCDIRECTION((0.,0.,1.));
#332= IFCCARTESIANPOINT((-3058.25300257,12869.3878261,0.));
#334= IFCAXIS2PLACEMENT3D(#332,#330,#328);
#335= IFCLOCALPLACEMENT(#165,#334);
#336= IFCCARTESIANPOINT((0.,-300.));
#338= IFCCARTESIANPOINT((8508.3373913,-300.));
#340= IFCCARTESIANPOINT((8508.3373913,0.));
#342= IFCCARTESIANPOINT((0.,0.));
#344= IFCPOLYLINE((#336,#338,#340,#342,#336));
#346= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#344);
#347= IFCDIRECTION((1.,0.,0.));
#349= IFCDIRECTION((0.,0.,1.));
#351= IFCCARTESIANPOINT((0.,0.,0.));
#353= IFCAXIS2PLACEMENT3D(#351,#349,#347);
#354= IFCDIRECTION((0.,0.,1.));
#356= IFCEXTRUDEDAREASOLID(#346,#353,#354,2700.);
#357= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#356));
#360= IFCCARTESIANPOINT((0.,0.));
#362= IFCCARTESIANPOINT((8508.3373913,0.));
#364= IFCPOLYLINE((#360,#362));
#366= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#364));
#369= IFCPRODUCTDEFINITIONSHAPE($,$,(#357,#366));
#373= IFCWALLSTANDARDCASE('3eT96F_cnDsOufCEsotwtp',#32,'YVT-B','',$,#335,#369,'E874918F-FA6C-4DD9-8E29-30EDB2DFADF3');
#377= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#378= IFCRELASSOCIATESMATERIAL('1iVlD94vOmLa5OpxnkCqlJ',#32,$,$,(#373),#377);
#381= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('2'),$);
#382= IFCPROPERTYSET('3lu_1RsD2PskLfQWcCOLU3',#32,'Reuse',$,(#381));
#384= IFCRELDEFINESBYPROPERTIES('1PufL50Yxw16Mg0x9OekT1',#32,$,$,(#373),#382);
#388= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#389= IFCPROPERTYSET('0221Oat$Xdl8OooXaoH0Gp',#32,'AC_Pset_RenovationAndPhasing',$,(#388));
#391= IFCRELDEFINESBYPROPERTIES('3OX_1Z$WvLZFlLD1nCbhRr',#32,$,$,(#373),#389);
#394= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#395= IFCPROPERTYSET('1T4$VD7m_rVNvlN5DrtbUq',#32,'Pset_WallCommon',$,(#394));
#397= IFCRELDEFINESBYPROPERTIES('1v2J9jM6ihU_rOwjxcKYIn',#32,$,$,(#373),#395);
#400= IFCQUANTITYLENGTH('Length',$,$,8508.3373913);
#401= IFCQUANTITYLENGTH('Height',$,$,2700.);
#402= IFCQUANTITYLENGTH('Width',$,$,300.);
#403= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.55250121739);
#404= IFCQUANTITYAREA('NetFootprintArea',$,$,2.55250121739);
#405= IFCQUANTITYAREA('GrossSideArea',$,$,22.9725109565);
#406= IFCQUANTITYAREA('NetSideArea',$,$,22.9725109565);
#407= IFCQUANTITYVOLUME('GrossVolume',$,$,6.89175328696);
#408= IFCQUANTITYVOLUME('NetVolume',$,$,6.89175328696);
#409= IFCELEMENTQUANTITY('09pcUknANnc6EQrUWIhXdt',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#400,#401,#402,#403,#404,#405,#406,#407,#408));
#411= IFCRELDEFINESBYPROPERTIES('2zE052B_urXe2bvDBLgYv1',#32,$,$,(#373),#409);
#414= IFCDIRECTION((1.,0.,0.));
#416= IFCDIRECTION((0.,0.,1.));
#418= IFCCARTESIANPOINT((35.6878669942,15007.8469565,0.));
#420= IFCAXIS2PLACEMENT3D(#418,#416,#414);
#421= IFCLOCALPLACEMENT(#165,#420);
#422= IFCCARTESIANPOINT((0.,-300.));
#424= IFCCARTESIANPOINT((7234.36173913,-300.));
#426= IFCCARTESIANPOINT((7234.36173913,0.));
#428= IFCCARTESIANPOINT((0.,0.));
#430= IFCPOLYLINE((#422,#424,#426,#428,#422));
#432= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#430);
#433= IFCDIRECTION((1.,0.,0.));
#435= IFCDIRECTION((0.,0.,1.));
#437= IFCCARTESIANPOINT((0.,0.,0.));
#439= IFCAXIS2PLACEMENT3D(#437,#435,#433);
#440= IFCDIRECTION((0.,0.,1.));
#442= IFCEXTRUDEDAREASOLID(#432,#439,#440,2700.);
#443= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#442));
#446= IFCCARTESIANPOINT((0.,0.));
#448= IFCCARTESIANPOINT((7234.36173913,0.));
#450= IFCPOLYLINE((#446,#448));
#452= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#450));
#455= IFCPRODUCTDEFINITIONSHAPE($,$,(#443,#452));
#459= IFCWALLSTANDARDCASE('3k3LbXmTfDWODxdSuyCGYE',#32,'YVT-C','',$,#421,#455,'EE0D5961-C1DA-4D81-837B-9DCE3C31088E');
#463= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#464= IFCRELASSOCIATESMATERIAL('3_dZLENyJ5_qngksl2ie6r',#32,$,$,(#459),#463);
#467= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('1'),$);
#468= IFCPROPERTYSET('342IhYDnCDoqxc_Zm6g_fw',#32,'Reuse',$,(#467));
#470= IFCRELDEFINESBYPROPERTIES('0KYu05w72mseS$C1XTnSzM',#32,$,$,(#459),#468);
#474= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#475= IFCPROPERTYSET('2POedyNP84Nk7S4D$CmpS2',#32,'AC_Pset_RenovationAndPhasing',$,(#474));
#477= IFCRELDEFINESBYPROPERTIES('2oUR0$VhKaEPV9FW3eb0G3',#32,$,$,(#459),#475);
#480= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#481= IFCPROPERTYSET('2IgxHtV7Xo2x_LGYkyiDI6',#32,'Pset_WallCommon',$,(#480));
#483= IFCRELDEFINESBYPROPERTIES('2d$ext$iF9gxEs6l5zaPYh',#32,$,$,(#459),#481);
#486= IFCQUANTITYLENGTH('Length',$,$,7234.36173913);
#487= IFCQUANTITYLENGTH('Height',$,$,2700.);
#488= IFCQUANTITYLENGTH('Width',$,$,300.);
#489= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.17030852174);
#490= IFCQUANTITYAREA('NetFootprintArea',$,$,2.17030852174);
#491= IFCQUANTITYAREA('GrossSideArea',$,$,19.5327766957);
#492= IFCQUANTITYAREA('NetSideArea',$,$,19.5327766957);
#493= IFCQUANTITYVOLUME('GrossVolume',$,$,5.8598330087);
#494= IFCQUANTITYVOLUME('NetVolume',$,$,5.8598330087);
#495= IFCELEMENTQUANTITY('1LnynQ0O$3P0FeM4JPb_Ao',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#486,#487,#488,#489,#490,#491,#492,#493,#494));
#497= IFCRELDEFINESBYPROPERTIES('14UKMwdhaOGUYk9crv2_wD',#32,$,$,(#459),#495);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,258 @@
ISO-10303-21;
HEADER;FILE_DESCRIPTION(('ViewDefinition [CoordinationView_V2.0, QuantityTakeOffAddOnView, SpaceBoundary2ndLevelAddOnView]','Option [Elements to export: Selected elements only]','Option [Partial Structure Display: Entire Model]','Option [IFC Domain: All]','Option [Structural Function: All Elements]','Option [Convert Grid elements: On]','Option [Convert IFC Annotations and ARCHICAD 2D elements: Off]','Option [Convert 2D symbols of Doors and Windows: On]','Option [Export geometries that Participates in Collision Detection only: Off]','Option [Split complex elements: Off]','Option [Material Preservation: Explode where necessary]','Option [Elements in Solid Element Operations: Extruded/revolved]','Option [Elements with junctions: Extruded/revolved without junctions]','Option [IFC Site Location: At Project Origin]','Option [Curtain Wall export mode: Container Element]','Option [Railing export mode: Single Element]','Option [Stair export mode: Single Element]','Option [Properties To Export: All properties]','Option [Space containment: Off]','Option [Bounding Box: Off]','Option [Geometry to type objects: Off]','Option [Element Properties: Off]','Option [Building Material Properties: Off]','Option [Element Parameters: Off]','Option [Component Parameters: Off]','Option [IFC Base Quantities: On]','Option [Door Window Parameters: Off]','Option [IFC Space boundaries: On]','Option [ARCHICAD Zone Categories as IFC Space classification data: On]','Option [Element Classifications: Off]'),'2;1');
FILE_NAME('C:\\Users\\Yoga\\Desktop\\test.ifc','2023-09-04T21:58:54',('Arkitekten'),('Arkitektkontoret'),'The EXPRESS Data Manager Version 5.02.0100.09 : 26 Sep 2013','IFC file generated by GRAPHISOFT ARCHICAD 25.0.0 NOR FULL Windows version (IFC add-on version: 3002 NOR FULL).','Arkitekten');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1= IFCACTORROLE(.USERDEFINED.,'Ark:',$);
#2= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#6= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#9= IFCPERSON($,$,'Arkitekten',$,$,$,(#1),(#2,#6));
#15= IFCPOSTALADDRESS(.USERDEFINED.,$,'Architect Postal Address',$,('Arkitektveien 19'),$,'Arkitektbyen',$,'0000',$);
#17= IFCTELECOMADDRESS(.USERDEFINED.,$,'Architect Telecom Address',('000 000 00'),$,$,('arkitekt@arkitektfirma.no'),'www.arkitektfirma.no');
#20= IFCORGANIZATION($,'Arkitektkontoret',$,$,(#15,#17));
#27= IFCPERSONANDORGANIZATION(#9,#20,$);
#30= IFCORGANIZATION('GS','GRAPHISOFT','GRAPHISOFT',$,$);
#31= IFCAPPLICATION(#30,'25.0.0','ARCHICAD','IFC add-on version: 3002 NOR FULL');
#32= IFCOWNERHISTORY(#27,#31,$,.NOCHANGE.,$,$,$,1693857534);
#33= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#34= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#35= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#36= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#37= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.0174532925199),#36);
#38= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#39= IFCCONVERSIONBASEDUNIT(#38,.PLANEANGLEUNIT.,'DEGREE',#37);
#40= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
#41= IFCMEASUREWITHUNIT(IFCPOSITIVELENGTHMEASURE(0.000304617419787),#40);
#42= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#43= IFCCONVERSIONBASEDUNIT(#42,.SOLIDANGLEUNIT.,'SQUAREDEGREE',#41);
#44= IFCMONETARYUNIT(.NOK.);
#45= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#46= IFCMEASUREWITHUNIT(IFCTIMEMEASURE(31556926.),#45);
#47= IFCDIMENSIONALEXPONENTS(0,0,1,0,0,0,0);
#48= IFCCONVERSIONBASEDUNIT(#47,.TIMEUNIT.,'Year',#46);
#49= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#50= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
#51= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
#52= IFCSIUNIT(*,.ENERGYUNIT.,.MEGA.,.JOULE.);
#53= IFCDERIVEDUNIT((#56,#58,#60),.THERMALCONDUCTANCEUNIT.,$);
#55= IFCSIUNIT(*,.POWERUNIT.,$,.WATT.);
#56= IFCDERIVEDUNITELEMENT(#55,1);
#57= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
#58= IFCDERIVEDUNITELEMENT(#57,-1);
#59= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#60= IFCDERIVEDUNITELEMENT(#59,-1);
#61= IFCDERIVEDUNIT((#64,#66,#68),.SPECIFICHEATCAPACITYUNIT.,$);
#63= IFCSIUNIT(*,.ENERGYUNIT.,$,.JOULE.);
#64= IFCDERIVEDUNITELEMENT(#63,1);
#65= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#66= IFCDERIVEDUNITELEMENT(#65,-1);
#67= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.KELVIN.);
#68= IFCDERIVEDUNITELEMENT(#67,-1);
#69= IFCDERIVEDUNIT((#72,#74),.MASSDENSITYUNIT.,$);
#71= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#72= IFCDERIVEDUNITELEMENT(#71,1);
#73= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#74= IFCDERIVEDUNITELEMENT(#73,-1);
#75= IFCUNITASSIGNMENT((#33,#34,#35,#39,#43,#44,#48,#49,#50,#51,#52,#53,#61,#69));
#77= IFCDIRECTION((1.,0.,0.));
#81= IFCDIRECTION((0.,0.,1.));
#83= IFCCARTESIANPOINT((0.,0.,0.));
#85= IFCAXIS2PLACEMENT3D(#83,#81,#77);
#86= IFCDIRECTION((0.,1.));
#88= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,0.01,#85,#86);
#91= IFCPROJECT('1CGyzxdzHaJybf9L4wHy3R',#32,'Prosjektnavn','Prosjektbeskrivelse',$,$,'Fase',(#88),#75);
#98= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#100= IFCDIRECTION((1.,0.,0.));
#102= IFCDIRECTION((0.,0.,1.));
#104= IFCCARTESIANPOINT((0.,0.,0.));
#106= IFCAXIS2PLACEMENT3D(#104,#102,#100);
#107= IFCLOCALPLACEMENT($,#106);
#110= IFCSITE('01uPthe3Uixx0TRWar3Vt8',#32,'Eiendomsnavn',$,$,#107,$,$,.ELEMENT.,(59,55,52,428000),(10,42,27,972000),0.,'',#98);
#116= IFCRELAGGREGATES('2F2UAtyl4uWjrKORQfJcEW',#32,$,$,#91,(#110));
#122= IFCQUANTITYLENGTH('GrossPerimeter',$,$,0.);
#124= IFCQUANTITYAREA('GrossArea',$,$,0.);
#125= IFCELEMENTQUANTITY('17rUPIjEMxVrkRJ11moELl',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#122,#124));
#130= IFCRELDEFINESBYPROPERTIES('0l65N8yZCPuwGi0lRf7Opq',#32,$,$,(#110),#125);
#134= IFCPOSTALADDRESS($,$,$,$,('Adresse'),'#','Sted',$,'0000','Norge');
#136= IFCDIRECTION((1.,0.,0.));
#138= IFCDIRECTION((0.,0.,1.));
#140= IFCCARTESIANPOINT((0.,0.,0.));
#142= IFCAXIS2PLACEMENT3D(#140,#138,#136);
#143= IFCLOCALPLACEMENT(#107,#142);
#145= IFCBUILDING('3AMeTOOFsdVLZuNV0$27s5',#32,'Bygningens navn','Bygningstype',$,#143,$,'Byggnummer',.ELEMENT.,$,$,#134);
#147= IFCRELAGGREGATES('0bntSkh7UDU_vAHXbaggwi',#32,$,$,#110,(#145));
#151= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#152= IFCELEMENTQUANTITY('3GDAgT3z640iFgZCkp8utf',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#151));
#154= IFCRELDEFINESBYPROPERTIES('3joCDTcjDP3x5zmHkLS3S0',#32,$,$,(#145),#152);
#158= IFCDIRECTION((1.,0.,0.));
#160= IFCDIRECTION((0.,0.,1.));
#162= IFCCARTESIANPOINT((0.,0.,1000.));
#164= IFCAXIS2PLACEMENT3D(#162,#160,#158);
#165= IFCLOCALPLACEMENT(#143,#164);
#167= IFCBUILDINGSTOREY('0St1hMhFUHZyrAECcby7I2',#32,'1. etasje',$,$,#165,$,$,.ELEMENT.,1000.);
#169= IFCRELAGGREGATES('0TRv38eowcVEYjXmnFlLK1',#32,$,$,#145,(#167));
#173= IFCQUANTITYLENGTH('NetHeight',$,$,2700.);
#174= IFCQUANTITYLENGTH('GrossHeight',$,$,2700.);
#175= IFCQUANTITYLENGTH('Height',$,$,2700.);
#176= IFCQUANTITYAREA('GrossFloorArea',$,$,0.);
#177= IFCELEMENTQUANTITY('0t$Y2LddO4Hpr3Foz8Be1$',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#173,#174,#175,#176));
#179= IFCRELDEFINESBYPROPERTIES('0J4XjFI$kRh2sJoL52j_PV',#32,$,$,(#167),#177);
#183= IFCDIRECTION((1.,0.,0.));
#185= IFCDIRECTION((0.,0.,1.));
#187= IFCCARTESIANPOINT((-4423.22691561,10230.4382609,0.));
#189= IFCAXIS2PLACEMENT3D(#187,#185,#183);
#190= IFCLOCALPLACEMENT(#165,#189);
#192= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#194= IFCCARTESIANPOINT((0.,-300.));
#196= IFCCARTESIANPOINT((8599.33565217,-300.));
#198= IFCCARTESIANPOINT((8599.33565217,0.));
#200= IFCCARTESIANPOINT((0.,0.));
#202= IFCPOLYLINE((#194,#196,#198,#200,#194));
#204= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#202);
#205= IFCDIRECTION((1.,0.,0.));
#207= IFCDIRECTION((0.,0.,1.));
#209= IFCCARTESIANPOINT((0.,0.,0.));
#211= IFCAXIS2PLACEMENT3D(#209,#207,#205);
#212= IFCDIRECTION((0.,0.,1.));
#214= IFCEXTRUDEDAREASOLID(#204,#211,#212,2700.);
#215= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#214));
#222= IFCPRESENTATIONLAYERASSIGNMENT('230- Yttervegger (som generisk objekt eller for gruppering)',$,(#215,#232,#357,#366,#443,#452),$);
#225= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Axis','Model',*,*,*,*,#88,$,.MODEL_VIEW.,$);
#226= IFCCARTESIANPOINT((0.,0.));
#228= IFCCARTESIANPOINT((8599.33565217,0.));
#230= IFCPOLYLINE((#226,#228));
#232= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#230));
#236= IFCPRODUCTDEFINITIONSHAPE($,$,(#215,#232));
#242= IFCWALLSTANDARDCASE('1Xezbq735AdBO_4DerG66G',#32,'YVT-A','',$,#190,#236,'61A3D974-1C31-4A9C-B63E-10DA35406190');
#257= IFCRELCONTAINEDINSPATIALSTRUCTURE('0PUNS2OuLwupG5o8M6iDor',#32,$,$,(#242,#373,#459),#167);
#261= IFCMATERIAL('Yttervegg');
#264= IFCCOLOURRGB($,1.,1.,1.);
#265= IFCSURFACESTYLERENDERING(#264,0.,IFCNORMALISEDRATIOMEASURE(0.3),$,$,$,IFCNORMALISEDRATIOMEASURE(0.69),$,.NOTDEFINED.);
#266= IFCSURFACESTYLE('Maling - 01 Blank',.BOTH.,(#265));
#268= IFCPRESENTATIONSTYLEASSIGNMENT((#266));
#270= IFCSTYLEDITEM($,(#268),$);
#272= IFCSTYLEDREPRESENTATION(#192,$,$,(#270));
#274= IFCMATERIALDEFINITIONREPRESENTATION($,$,(#272),#261);
#278= IFCMATERIALLAYER(#261,300.,.U.);
#280= IFCMATERIALLAYERSET((#278),'Yttervegg 300');
#283= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#284= IFCRELASSOCIATESMATERIAL('2msyZzaqHpLqjyOSMDJ6bC',#32,$,$,(#242),#283);
#287= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('3'),$);
#291= IFCPROPERTYSET('2axpxSTK16Nv0$I70apGdK',#32,'Reuse',$,(#287));
#293= IFCRELDEFINESBYPROPERTIES('36a5T_GLs751QxsAvh1iEB',#32,$,$,(#242),#291);
#297= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#298= IFCPROPERTYSET('04Pv5xI9$vG1lZdN7wMAEc',#32,'AC_Pset_RenovationAndPhasing',$,(#297));
#300= IFCRELDEFINESBYPROPERTIES('2AvYgZFQ$eywkTT_yzugG4',#32,$,$,(#242),#298);
#303= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#304= IFCPROPERTYSET('3vuFSkrI50S$hRePMCpJTW',#32,'Pset_WallCommon',$,(#303));
#306= IFCRELDEFINESBYPROPERTIES('3eVxLUBuO61JJXioN57x5f',#32,$,$,(#242),#304);
#309= IFCQUANTITYLENGTH('Length',$,$,8599.33565217);
#310= IFCQUANTITYLENGTH('Height',$,$,2700.);
#311= IFCQUANTITYLENGTH('Width',$,$,300.);
#312= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.57980069565);
#313= IFCQUANTITYAREA('NetFootprintArea',$,$,2.57980069565);
#314= IFCQUANTITYAREA('GrossSideArea',$,$,23.2182062609);
#315= IFCQUANTITYAREA('NetSideArea',$,$,23.2182062609);
#316= IFCQUANTITYVOLUME('GrossVolume',$,$,6.96546187826);
#317= IFCQUANTITYVOLUME('NetVolume',$,$,6.96546187826);
#318= IFCELEMENTQUANTITY('1uJa0ITKB1qrAAf8WtmaZ2',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#309,#310,#311,#312,#313,#314,#315,#316,#317));
#320= IFCRELDEFINESBYPROPERTIES('3eMBYSfULCCgJUrbtWuHgF',#32,$,$,(#242),#318);
#323= IFCWALLTYPE('2V6t5i6hK0w_M3VKaOTPG8',#32,'Yttervegg 300',$,$,$,$,'9F1B716C-1AB5-00EB-E583-7D4918759408',$,.NOTDEFINED.);
#325= IFCRELDEFINESBYTYPE('0GIkSqbN66cNEifXw6t8R_',#32,$,$,(#242,#373,#459),#323);
#328= IFCDIRECTION((1.,0.,0.));
#330= IFCDIRECTION((0.,0.,1.));
#332= IFCCARTESIANPOINT((-3058.25300257,12869.3878261,0.));
#334= IFCAXIS2PLACEMENT3D(#332,#330,#328);
#335= IFCLOCALPLACEMENT(#165,#334);
#336= IFCCARTESIANPOINT((0.,-300.));
#338= IFCCARTESIANPOINT((8508.3373913,-300.));
#340= IFCCARTESIANPOINT((8508.3373913,0.));
#342= IFCCARTESIANPOINT((0.,0.));
#344= IFCPOLYLINE((#336,#338,#340,#342,#336));
#346= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#344);
#347= IFCDIRECTION((1.,0.,0.));
#349= IFCDIRECTION((0.,0.,1.));
#351= IFCCARTESIANPOINT((0.,0.,0.));
#353= IFCAXIS2PLACEMENT3D(#351,#349,#347);
#354= IFCDIRECTION((0.,0.,1.));
#356= IFCEXTRUDEDAREASOLID(#346,#353,#354,2700.);
#357= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#356));
#360= IFCCARTESIANPOINT((0.,0.));
#362= IFCCARTESIANPOINT((8508.3373913,0.));
#364= IFCPOLYLINE((#360,#362));
#366= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#364));
#369= IFCPRODUCTDEFINITIONSHAPE($,$,(#357,#366));
#373= IFCWALLSTANDARDCASE('3eT96F_cnDsOufCEsotwtp',#32,'YVT-B','',$,#335,#369,'E874918F-FA6C-4DD9-8E29-30EDB2DFADF3');
#377= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#378= IFCRELASSOCIATESMATERIAL('1iVlD94vOmLa5OpxnkCqlJ',#32,$,$,(#373),#377);
#381= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('2'),$);
#382= IFCPROPERTYSET('3lu_1RsD2PskLfQWcCOLU3',#32,'Reuse',$,(#381));
#384= IFCRELDEFINESBYPROPERTIES('1PufL50Yxw16Mg0x9OekT1',#32,$,$,(#373),#382);
#388= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#389= IFCPROPERTYSET('0221Oat$Xdl8OooXaoH0Gp',#32,'AC_Pset_RenovationAndPhasing',$,(#388));
#391= IFCRELDEFINESBYPROPERTIES('3OX_1Z$WvLZFlLD1nCbhRr',#32,$,$,(#373),#389);
#394= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#395= IFCPROPERTYSET('1T4$VD7m_rVNvlN5DrtbUq',#32,'Pset_WallCommon',$,(#394));
#397= IFCRELDEFINESBYPROPERTIES('1v2J9jM6ihU_rOwjxcKYIn',#32,$,$,(#373),#395);
#400= IFCQUANTITYLENGTH('Length',$,$,8508.3373913);
#401= IFCQUANTITYLENGTH('Height',$,$,2700.);
#402= IFCQUANTITYLENGTH('Width',$,$,300.);
#403= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.55250121739);
#404= IFCQUANTITYAREA('NetFootprintArea',$,$,2.55250121739);
#405= IFCQUANTITYAREA('GrossSideArea',$,$,22.9725109565);
#406= IFCQUANTITYAREA('NetSideArea',$,$,22.9725109565);
#407= IFCQUANTITYVOLUME('GrossVolume',$,$,6.89175328696);
#408= IFCQUANTITYVOLUME('NetVolume',$,$,6.89175328696);
#409= IFCELEMENTQUANTITY('09pcUknANnc6EQrUWIhXdt',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#400,#401,#402,#403,#404,#405,#406,#407,#408));
#411= IFCRELDEFINESBYPROPERTIES('2zE052B_urXe2bvDBLgYv1',#32,$,$,(#373),#409);
#414= IFCDIRECTION((1.,0.,0.));
#416= IFCDIRECTION((0.,0.,1.));
#418= IFCCARTESIANPOINT((35.6878669942,15007.8469565,0.));
#420= IFCAXIS2PLACEMENT3D(#418,#416,#414);
#421= IFCLOCALPLACEMENT(#165,#420);
#422= IFCCARTESIANPOINT((0.,-300.));
#424= IFCCARTESIANPOINT((7234.36173913,-300.));
#426= IFCCARTESIANPOINT((7234.36173913,0.));
#428= IFCCARTESIANPOINT((0.,0.));
#430= IFCPOLYLINE((#422,#424,#426,#428,#422));
#432= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'',#430);
#433= IFCDIRECTION((1.,0.,0.));
#435= IFCDIRECTION((0.,0.,1.));
#437= IFCCARTESIANPOINT((0.,0.,0.));
#439= IFCAXIS2PLACEMENT3D(#437,#435,#433);
#440= IFCDIRECTION((0.,0.,1.));
#442= IFCEXTRUDEDAREASOLID(#432,#439,#440,2700.);
#443= IFCSHAPEREPRESENTATION(#192,'Body','SweptSolid',(#442));
#446= IFCCARTESIANPOINT((0.,0.));
#448= IFCCARTESIANPOINT((7234.36173913,0.));
#450= IFCPOLYLINE((#446,#448));
#452= IFCSHAPEREPRESENTATION(#225,'Axis','Curve2D',(#450));
#455= IFCPRODUCTDEFINITIONSHAPE($,$,(#443,#452));
#459= IFCWALLSTANDARDCASE('3k3LbXmTfDWODxdSuyCGYE',#32,'YVT-C','',$,#421,#455,'EE0D5961-C1DA-4D81-837B-9DCE3C31088E');
#463= IFCMATERIALLAYERSETUSAGE(#280,.AXIS2.,.NEGATIVE.,0.);
#464= IFCRELASSOCIATESMATERIAL('3_dZLENyJ5_qngksl2ie6r',#32,$,$,(#459),#463);
#467= IFCPROPERTYSINGLEVALUE('GTIN',$,IFCLABEL('1'),$);
#468= IFCPROPERTYSET('342IhYDnCDoqxc_Zm6g_fw',#32,'Reuse',$,(#467));
#470= IFCRELDEFINESBYPROPERTIES('0KYu05w72mseS$C1XTnSzM',#32,$,$,(#459),#468);
#474= IFCPROPERTYSINGLEVALUE('Renovation Status',$,IFCLABEL('New'),$);
#475= IFCPROPERTYSET('2POedyNP84Nk7S4D$CmpS2',#32,'AC_Pset_RenovationAndPhasing',$,(#474));
#477= IFCRELDEFINESBYPROPERTIES('2oUR0$VhKaEPV9FW3eb0G3',#32,$,$,(#459),#475);
#480= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
#481= IFCPROPERTYSET('2IgxHtV7Xo2x_LGYkyiDI6',#32,'Pset_WallCommon',$,(#480));
#483= IFCRELDEFINESBYPROPERTIES('2d$ext$iF9gxEs6l5zaPYh',#32,$,$,(#459),#481);
#486= IFCQUANTITYLENGTH('Length',$,$,7234.36173913);
#487= IFCQUANTITYLENGTH('Height',$,$,2700.);
#488= IFCQUANTITYLENGTH('Width',$,$,300.);
#489= IFCQUANTITYAREA('GrossFootprintArea',$,$,2.17030852174);
#490= IFCQUANTITYAREA('NetFootprintArea',$,$,2.17030852174);
#491= IFCQUANTITYAREA('GrossSideArea',$,$,19.5327766957);
#492= IFCQUANTITYAREA('NetSideArea',$,$,19.5327766957);
#493= IFCQUANTITYVOLUME('GrossVolume',$,$,5.8598330087);
#494= IFCQUANTITYVOLUME('NetVolume',$,$,5.8598330087);
#495= IFCELEMENTQUANTITY('1LnynQ0O$3P0FeM4JPb_Ao',#32,'BaseQuantities',$,'ARCHICAD BIM Base Quantities',(#486,#487,#488,#489,#490,#491,#492,#493,#494));
#497= IFCRELDEFINESBYPROPERTIES('14UKMwdhaOGUYk9crv2_wD',#32,$,$,(#459),#495);
ENDSEC;
END-ISO-10303-21;
+120
View File
@@ -0,0 +1,120 @@
import jsonpickle
import os
import pytz
from dateutil import parser
from uuid import uuid4, UUID
from neo4j import GraphDatabase
from datetime import datetime, timezone
from models.request import UserInDB
from security.secrets import get_secrets
get_secrets()
# neo4j://127.0.0.1:27687 is used when there are no environment variables, i.e. when running from terminal.
# otherwise the environment variable will have a value of neo4j://kontroll_neo4j:27687
# kontroll_neo4j is the docker-compose network.
driver = GraphDatabase.driver(os.environ['NEO4J_URI'],
auth=(os.environ['NEO4J_USER'],
os.environ['NEO4J_INITIAL_PASSWORD']))
# initial password should be changed to secret password
driver.verify_connectivity()
cypher_file_path = "./db_config/init.cypher"
class MyDB:
def __init__(self, object_driver):
self.driver = object_driver
self.database = 'neo4j'
@staticmethod
def timestamp():
return datetime.now(timezone.utc).isoformat(sep='T', timespec='milliseconds')
@staticmethod
def bcf_time(any_datetime):
if isinstance(any_datetime, type('str')):
datetime_any = parser.parse(any_datetime).astimezone(pytz.utc)
elif isinstance(any_datetime, type(datetime.now())):
datetime_any = any_datetime.astimezone(pytz.utc)
else:
return False
string_date = datetime_any.isoformat(sep='T', timespec='milliseconds')
return str(string_date)
@staticmethod
def safe_path(path_name):
safe_path_name = ''.join(x for x in path_name if x.isalnum() or '-')
return safe_path_name
@staticmethod
def debug(endpoint: str, request, response):
print("\n\n\nEndpoint: ", jsonpickle.dumps(endpoint),
"\nRequest: ", jsonpickle.dumps(request),
"\nResponse: ", jsonpickle.dumps(response))
@staticmethod
def node_to_json(node):
json = {}
for fields in node.items():
json[fields[0]] = fields[1]
return json
@staticmethod
def is_valid_uuid(uuid_to_test, version=4):
try:
uuid_obj = UUID(uuid_to_test, version=version)
except ValueError:
return False
return str(uuid_obj) == uuid_to_test
@staticmethod
def new_uuid():
try:
uuid_obj = uuid4()
except ValueError:
return False
return str(uuid_obj)
def initialize_db(self):
def initialize_db_work(tx):
cypher_file = open(cypher_file_path, "r")
cypher_data = cypher_file.read()
cypher_file.close()
cypher_statements = cypher_data.split(';')
cypher_statements.pop()
for cypher_statement in cypher_statements:
tx.run(cypher_statement)
return
with self.driver.session() as session:
return session.execute_write(initialize_db_work)
def get_user(self, username: str):
def get_user_work(tx, username_work: str):
cypher = """
MATCH (u:User)
WHERE u.username = $username
RETURN u AS user
LIMIT 1
"""
result = tx.run(cypher, username=username_work)
first = result.single()
if first is None:
return None
user_node = first.get("user")
user_dict = self.node_to_json(user_node)
user = UserInDB(**user_dict)
return user
with self.driver.session() as session:
return session.execute_read(get_user_work, username_work=username)
db = MyDB(driver)
db.initialize_db()
@@ -0,0 +1,655 @@
{
"style": {
"font-family": "sans-serif",
"background-color": "#ffffff",
"background-image": "",
"background-size": "100%",
"node-color": "#ffffff",
"border-width": 4,
"border-color": "#000000",
"radius": 50,
"node-padding": 5,
"node-margin": 2,
"outside-position": "auto",
"node-icon-image": "",
"node-background-image": "",
"icon-position": "inside",
"icon-size": 64,
"caption-position": "inside",
"caption-max-width": 200,
"caption-color": "#000000",
"caption-font-size": 50,
"caption-font-weight": "normal",
"label-position": "inside",
"label-display": "pill",
"label-color": "#000000",
"label-background-color": "#ffffff",
"label-border-color": "#000000",
"label-border-width": 4,
"label-font-size": 40,
"label-padding": 5,
"label-margin": 4,
"directionality": "directed",
"detail-position": "inline",
"detail-orientation": "parallel",
"arrow-width": 5,
"arrow-color": "#000000",
"margin-start": 5,
"margin-end": 5,
"margin-peer": 20,
"attachment-start": "normal",
"attachment-end": "normal",
"relationship-icon-image": "",
"type-color": "#000000",
"type-background-color": "#ffffff",
"type-border-color": "#000000",
"type-border-width": 0,
"type-font-size": 16,
"type-padding": 5,
"property-position": "outside",
"property-alignment": "colon",
"property-color": "#000000",
"property-font-size": 16,
"property-font-weight": "normal"
},
"nodes": [
{
"id": "n0",
"position": {
"x": -289.1004788300022,
"y": 479.89101705018163
},
"caption": "Topic",
"style": {},
"labels": [
"Topic"
],
"properties": {
"guid": "STRING",
"creation_date": "",
"modified_date": "",
"creation_author": "",
"modified_author": "",
"topic_type": "",
"topic_status": "",
"title": "",
"priority": "",
"index": "",
"assigned_to": "",
"stage": "",
"description": "",
"due_date": ""
}
},
{
"id": "n1",
"position": {
"x": -333.1242108163427,
"y": 63.328364869784195
},
"caption": "Project",
"style": {},
"labels": [
"Project"
],
"properties": {
"project_id": "STRING",
"name": "STRING"
}
},
{
"id": "n2",
"position": {
"x": 16.812745060861175,
"y": -286.6085910074197
},
"caption": "Extensions",
"style": {},
"labels": [
"Extensions"
],
"properties": {
"topic_type": "",
"topic_status": "",
"topic_label": "",
"snippet_type": "",
"priority": "",
"users": "",
"stage": "",
"project_actions": "",
"topic_actions": "",
"comment_actions": ""
}
},
{
"id": "n3",
"position": {
"x": 226.07663507128208,
"y": 479.89101705018163
},
"caption": "Viewpoint",
"style": {},
"labels": [
"Viewpoint"
],
"properties": {
"guid": "",
"snapshot": "",
"snapshot_type": "",
"index": "",
"camera_definition": "",
"camera_view_point": "",
"camera_direction": "",
"camera_up_vector": "",
"view_tws_or_fo_view": "",
"aspect_ratio": "",
"default_visibility": "",
"spaces_visible": "",
"space_boundaries_visible": "",
"openings_visible": ""
}
},
{
"id": "n4",
"position": {
"x": -1063.7571554834985,
"y": -259.5570287821265
},
"caption": "User",
"style": {},
"labels": [
"User"
],
"properties": {
"UUID": "",
"username": "STRING",
"full_name": "",
"email": "",
"hashed_password": "",
"disabled": ""
}
},
{
"id": "n5",
"position": {
"x": 529.1987482469625,
"y": 63.328364869784195
},
"caption": "Component",
"style": {},
"labels": [
"Component"
],
"properties": {
"ifc_guid": "",
"selected": "",
"originating_system": "",
"authoring_tool_id": "",
"color": "",
"visibility_exception": "",
"GTIN": ""
}
},
{
"id": "n6",
"position": {
"x": 264.11970819463727,
"y": -73.26061694118164
},
"caption": "Comment",
"style": {},
"labels": [
"Comment"
],
"properties": {
"guid": "",
"reply_to_comment_guid": "",
"date": "",
"modified_date": "",
"author": "",
"modified_author": "",
"comment": ""
}
},
{
"id": "n7",
"position": {
"x": -651.5332260698573,
"y": -497.5546253954978
},
"caption": "Token:AcessToken",
"style": {},
"labels": [
"Token:AcessToken"
],
"properties": {
"code": "",
"scope": "",
"value": "",
"hash": ""
}
},
{
"id": "n8",
"position": {
"x": -485.20568785423296,
"y": -367.1506322968085
},
"caption": "Token:RefreshToken",
"style": {},
"labels": [
"Token:RefreshToken"
],
"properties": {
"code": "",
"scope": "",
"value": "",
"hash": ""
}
},
{
"id": "n9",
"position": {
"x": -192.7032908251408,
"y": -331.31640881713093
},
"caption": "Process",
"style": {},
"labels": [
"Process"
],
"properties": {}
},
{
"id": "n10",
"position": {
"x": -1507.6552365118007,
"y": -73.26061694118164
},
"caption": "Session:Upload",
"style": {},
"labels": [
"Session:Upload"
],
"properties": {
"server_context": "",
"upload_session": "",
"callback": "",
"session_url_timedelta": "",
"session_callback_timedelta": ""
}
},
{
"id": "n11",
"position": {
"x": -1063.7571554834985,
"y": 370.6374640871204
},
"caption": "Document:Model:Temporary",
"style": {},
"labels": [
"Document:Model"
],
"properties": {
"document_id": "",
"version_index": "",
"ifc_project": "",
"name": "",
"title": "",
"guid": "",
"url": "",
"description": "",
"session_file_id": "",
"version_number": "",
"creation_date": "",
"original_file_name": "",
"file_ending": "",
"mime_type": "",
"file_type": "",
"size_in_bytes": "",
"project": "",
"reference": "",
"date": "",
"ifc_spatial_structure_element": "",
"filename": ""
}
},
{
"id": "n12",
"position": {
"x": -1352.4885415635456,
"y": 659.3688501671675
},
"caption": "Part",
"style": {},
"labels": [
"Part"
],
"properties": {
"uuid": "",
"number": "",
"content_range_start": "",
"content_range_end": "",
"content_length": "",
"uploaded": "Boolean"
}
},
{
"id": "n13",
"position": {
"x": -954.4308563764228,
"y": 63.328364869784195
},
"caption": "Session:Select",
"style": {},
"labels": [
"Session:Select"
],
"properties": {
"server_context": "",
"selection_session": "",
"callback": "",
"session_url_timedelta": "",
"session_callback_timedelta": ""
}
},
{
"id": "n14",
"position": {
"x": -1217.4300471311708,
"y": -21.114959701631534
},
"caption": "Link",
"style": {},
"labels": [
"Link"
],
"properties": {}
},
{
"id": "n15",
"position": {
"x": 570.3648741605024,
"y": 533.5911545751243
},
"caption": "Line",
"style": {},
"labels": [
"Line"
],
"properties": {
"start_point": "",
"end_point": ""
}
},
{
"id": "n16",
"position": {
"x": 645.5935574267153,
"y": 299.3814975504374
},
"caption": "ClippingPlane",
"style": {},
"labels": [
"CilppingPlane"
],
"properties": {
"location": "",
"direction": ""
}
},
{
"id": "n17",
"position": {
"x": 425.3162762641219,
"y": 765.7610254853134
},
"caption": "Bitmap",
"style": {},
"labels": [
"Bitmap"
],
"properties": {
"type": "",
"location": "",
"normal": "",
"up": "",
"height": ""
}
},
{
"id": "n18",
"position": {
"x": 645.5935574267153,
"y": -331.31640881713093
},
"caption": "IfcProduct",
"style": {},
"labels": [
"IfcProduct"
],
"properties": {
"GlobalId": ""
}
},
{
"id": "n19",
"position": {
"x": -988.4895851499441,
"y": -536.5949869348817
},
"caption": "AuthorizationCode",
"style": {},
"labels": [],
"properties": {
"code": ""
}
}
],
"relationships": [
{
"id": "n0",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n1",
"toId": "n0"
},
{
"id": "n1",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n1",
"toId": "n2"
},
{
"id": "n2",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n0",
"toId": "n3"
},
{
"id": "n3",
"type": "HAS_ACTIONS_ON",
"style": {},
"properties": {
"read": "BOOLEAN",
"update": "BOOLEAN",
"createTopic": "BOOLEAN",
"createDocument": "BOOLEAN",
"createViewpoint": "BOOLEAN",
"createComment": "BOOLEAN",
"updateDocumentReferences": "BOOLEAN",
"updateFiles": "BOOLEAN",
"updateRelatedTopics": "BOOLEAN"
},
"fromId": "n4",
"toId": "n1"
},
{
"id": "n4",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n3",
"toId": "n5"
},
{
"id": "n5",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n0",
"toId": "n6"
},
{
"id": "n6",
"type": "RELATED_TO",
"style": {},
"properties": {},
"fromId": "n6",
"toId": "n3"
},
{
"id": "n7",
"type": "CAN_USE",
"style": {},
"properties": {},
"fromId": "n4",
"toId": "n7"
},
{
"id": "n8",
"type": "CAN_USE",
"style": {},
"properties": {},
"fromId": "n4",
"toId": "n8"
},
{
"id": "n9",
"type": "CONTAINS",
"style": {},
"properties": {},
"fromId": "n9",
"toId": "n1"
},
{
"id": "n10",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n4",
"toId": "n10"
},
{
"id": "n11",
"type": "CONTAINS",
"style": {},
"properties": {},
"fromId": "n10",
"toId": "n11"
},
{
"id": "n12",
"type": "REFERS_TO",
"style": {},
"properties": {
"guid": ""
},
"fromId": "n0",
"toId": "n11"
},
{
"id": "n13",
"type": "CONTAINS",
"style": {},
"properties": {},
"fromId": "n1",
"toId": "n11"
},
{
"id": "n14",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n11",
"toId": "n12"
},
{
"id": "n15",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n4",
"toId": "n13"
},
{
"id": "n16",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n10",
"toId": "n14"
},
{
"id": "n17",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n13",
"toId": "n14"
},
{
"id": "n18",
"type": "SELECTED",
"style": {},
"properties": {},
"fromId": "n13",
"toId": "n11"
},
{
"id": "n19",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n3",
"toId": "n15"
},
{
"id": "n20",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n3",
"toId": "n16"
},
{
"id": "n21",
"type": "HAS",
"style": {},
"properties": {},
"fromId": "n3",
"toId": "n17"
},
{
"id": "n22",
"type": "SAME_AS",
"style": {},
"properties": {},
"fromId": "n5",
"toId": "n18"
},
{
"id": "n23",
"type": "CAN_USE",
"style": {},
"properties": {},
"fromId": "n4",
"toId": "n19"
}
]
}
@@ -0,0 +1,94 @@
MATCH (n) DETACH DELETE n;
CREATE
(user1:User:Admin {UUID: 1, username: 'admin', full_name: "Admin Sliter", email: "admin@verket.se", hashed_password: "$2a$10$SyekXmrbf9rEd9KrfcXQd.gNWM8iRl228nPTrxqJq0wlDamlHzDly", disabled: false}),
(user2:User:Officer {UUID: 2, username: 'kommun', full_name: "Kalle Nitisk", email: "kalle.nitisk@kommunen.se", hashed_password: "$2a$10$lX1BIZZuBGOIEiHCxXwrf.IoeHOAWgB3ri6vX10xtMVEmzVH/GbCu", disabled: false}),
(user3:User:Developer {UUID: 3, username: 'byggherre', full_name: "Byggherre Stolt", email: "stolt@byggfirman.com", hashed_password: "$2a$10$i08DcyX2r9CeWBPaUykDeeynvI5/Aa9scRfi.MfbTgCGm/XqcpGcG", disabled: false}),
(user4:User:Auditor {UUID: 4, username: 'kontrollansvarig', full_name: "Kontrollansvarig Tapper", email: "kontrollansvarig@ansvaret.se", hashed_password: "$2a$10$OlfSbVqpOmuNNf8KHxFXhONU70nxbrMSH5vL2uc9ydCDfJVAP/gl6", disabled: false}),
(user5:User:Tester {UUID: 5, username: 'kontrollant', full_name: "Finn Failer", email: "finn.failer@byggfirman.com", hashed_password: "$2a$10$lX1BIZZuBGOIEiHCxXwrf.IoeHOAWgB3ri6vX10xtMVEmzVH/GbCu", disabled: false}),
(municipality1:Municipality {UUID: 6, name: 'Karlskrona'}),
(building1:Building {UUID: 7, status: "Under construction"}),
(project1:Project {project_id: 'c8e9ccce-a4af-11ed-b9df-0242ac120003', name: 'Bygglov'}),
(project2:Project {project_id: 'd1842bfe-a4af-11ed-b9df-0242ac120003', name: 'Rivningslov'}),
(document1:Document {
document_id: '78860d3c-14db-11ee-be56-0242ac120002',
session_file_id: '',
version_index: 1,
version_number: 'Första',
creation_date: '2023-01-01T01:01:01.111',
title: 'Solibri building',
original_file_name: 'Solibri_building.ifc',
file_ending: '.ifc',
mime_type: 'application/x-step',
file_type: 'STEP Physical File (SPF)',
name: '78860d3c-14db-11ee-be56-0242ac120002.ifc',
size_in_bytes: 16962975,
ifc_project: "0g8GxLEzP459ZWW6_RGsez"
}),
(document2:Document {
document_id: '6dbd4d52-14db-11ee-be56-0242ac120002',
session_file_id: '',
version_index: 2,
version_number: 'Andra',
creation_date: '2023-01-01T01:01:01.111',
title: 'Office building',
original_file_name: 'Office_building.ifc',
file_ending: '.ifc',
mime_type: 'application/x-step',
file_type: 'STEP Physical File (SPF)',
name: '6dbd4d52-14db-11ee-be56-0242ac120002.ifc',
size_in_bytes: 8332380,
ifc_project: "2gszwez3bE580ABnUBdjoQ"
}),
(document3:Document {
document_id: '78860e68-14db-11ee-be56-0242ac120002',
session_file_id: '',
version_index: 3,
version_number: 'Tredje',
creation_date: '2023-01-01T01:01:01:01.111',
title: 'Solibri structural',
original_file_name: 'Solibri_structural.ifc',
file_ending: '.ifc',
mime_type: 'application/x-step',
file_type: 'STEP Physical File (SPF)',
name: '78860e68-14db-11ee-be56-0242ac120002.ifc',
size_in_bytes: 146606,
ifc_project: "19sOQo3B98PAcnw01BM8Xo"
}),
(user2)-[r4:HAS_ACTIONS_ON {
read: True,
update: True,
createTopic: True,
createDocument: True,
createViewpoint: True,
createComment: True
}]->(project1),
(user2)-[r8:HAS_ACTIONS_ON {
read: True,
update: True,
createTopic: True,
createDocument: True,
createViewpoint: True,
createComment: True
}]->(project2),
(user3)-[r9:HAS_ACTIONS_ON {
read: True,
update: True,
createTopic: True,
createDocument: True,
createViewpoint: True,
createComment: True
}]->(project1),
(project1)-[r10:HAS]->(e1:Extensions {
topic_type: ["Information", "Error"],
custom_information: ["Custom value 1", "Custom value 2"],
topic_status: ["Open", "Closed", "ReOpened", "Custom test status"],
topic_label: ["Architecture", "Structural", "MEP"],
snippet_type: [".ifc", ".csv"],
priority: ["Low", "Medium", "High"],
users: ["kommun", "byggherre", "kontrollansvarig", "kontrollant", "martin@wiss.se"],
stage: ["Preliminary Planning End", "Construction Start", "Construction End"],
project_actions: ["update", "createTopic", "createDocument"],
topic_actions: ["update", "updateBimSnippet", "updateRelatedTopics", "updateDocumentReferences", "updateFiles", "createComment", "createViewpoint"],
comment_actions: ["update"]}),
(project1)-[r11:CONTAINS]->(document1),
(project1)-[r12:CONTAINS]->(document2);
@@ -0,0 +1,9 @@
MATCH (n) DETACH DELETE n;
CREATE (:Extensions {topic_type: "", topic_status: "", topic_label: "", snippet_type: "", priority: "", users: "", stage: "", project_actions: "", topic_actions: "", comment_actions: ""})<-[:HAS]-(Project:Project {project_id: "STRING", name: "STRING"})-[:HAS]->(Topic:Topic {guid: "STRING", creation_date: "", modified_date: "", creation_author: "", modified_author: "", topic_type: "", topic_status: "", title: "", priority: "", index: "", assigned_to: "", stage: "", description: "", due_date: ""})-[:HAS]->(Viewpoint:Viewpoint {guid: "", snapshot: "", snapshot_type: "", index: "", camera_definition: "", camera_view_point: "", camera_direction: "", camera_up_vector: "", view_tws_or_fo_view: "", aspect_ratio: "", default_visibility: "", spaces_visible: "", space_boundaries_visible: "", openings_visible: ""})-[:HAS]->(:Component {ifc_guid: "", selected: "", originating_system: "", authoring_tool_id: "", color: "", visibility_exception: "", GTIN: ""})-[:SAME_AS]->(:IfcProduct {GlobalId: ""}),
(:`Token:AcessToken` {code: "", scope: "", value: "", hash: ""})<-[:CAN_USE]-(User:User {UUID: "", username: "STRING", full_name: "", email: "", hashed_password: "", disabled: ""})-[:HAS_ACTIONS_ON {read: "BOOLEAN", update: "BOOLEAN", createTopic: "BOOLEAN", createDocument: "BOOLEAN", createViewpoint: "BOOLEAN", createComment: "BOOLEAN", updateDocumentReferences: "BOOLEAN", updateFiles: "BOOLEAN", updateRelatedTopics: "BOOLEAN"}]->(Project)<-[:CONTAINS]-(:Process),
(Project)-[:CONTAINS]->(`Document:Model:Temporary`:`Document:Model` {document_id: "", version_index: "", ifc_project: "", name: "", title: "", guid: "", url: "", description: "", session_file_id: "", version_number: "", creation_date: "", original_file_name: "", file_ending: "", mime_type: "", file_type: "", size_in_bytes: "", project: "", reference: "", date: "", ifc_spatial_structure_element: "", filename: ""})<-[:REFERS_TO {guid: ""}]-(Topic)-[:HAS]->(:Comment {guid: "", reply_to_comment_guid: "", date: "", modified_date: "", author: "", modified_author: "", comment: ""})-[:RELATED_TO]->(Viewpoint)-[:HAS]->(:Line {start_point: "", end_point: ""}),
(:Part {uuid: "", number: "", content_range_start: "", content_range_end: "", content_length: "", uploaded: "Boolean"})<-[:HAS]-(`Document:Model:Temporary`)<-[:CONTAINS]-(`Session:Upload`:`Session:Upload` {server_context: "", upload_session: "", callback: "", session_url_timedelta: "", session_callback_timedelta: ""})<-[:HAS]-(User)-[:CAN_USE]->(:`Token:RefreshToken` {code: "", scope: "", value: "", hash: ""}),
({code: ""})<-[:CAN_USE]-(User)-[:HAS]->(`Session:Select`:`Session:Select` {server_context: "", selection_session: "", callback: "", session_url_timedelta: "", session_callback_timedelta: ""})-[:HAS]->(Link:Link),
(`Session:Upload`)-[:HAS]->(Link),
(`Session:Select`)-[:SELECTED]->(`Document:Model:Temporary`),
(:Bitmap {type: "", location: "", normal: "", up: "", height: ""})<-[:HAS]-(Viewpoint)-[:HAS]->(:CilppingPlane {location: "", direction: ""})
@@ -0,0 +1,89 @@
# The code in this file originally comes from the following article:
#
# IFC-graph for facilitating building information access and query
#
# Junxiang Zhu, Peng Wu *, Xiang Lei
#
# School of Design and the Built Environment, Curtin University,
# Bentley 6102, Western Australia, Australia
#
# The article was made available online 13 February 2023 in the journal
# Automation in Construction 148 (2023) 104778
#
# 0926-5805/© 2023 The Authors.
# Published by Elsevier B.V.
#
# This is an open access article under the CC BY license (http://creativecommons.org/licenses/by/4.0/).
#
# Some modifications have been made to the code.
#
from py2neo.data import Node, Relationship
from uuid import uuid4
import ifcopenshell
from py2neo import Graph
# Create the basic node with literal attributes and the class hierarchy
def create_pure_node_from_ifc_entity(ifc_entity, ifc_file, hierarchy=True):
node = Node()
if ifc_entity.id() != 0:
node['id'] = ifc_entity.id()
else:
node['id'] = str(uuid4())
node['name'] = ifc_entity.is_a()
if hierarchy:
for label in ifc_file.wrapped_data.types_with_super():
if ifc_entity.is_a(label):
node.add_label(label)
else:
node.add_label(ifc_entity.is_a())
attributes_type = ['ENTITY INSTANCE', 'AGGREGATE OF ENTITY INSTANCE', 'DERIVED']
for i in range(ifc_entity.__len__()):
if not ifc_entity.wrapped_data.get_argument_type(i) in attributes_type:
name = ifc_entity.wrapped_data.get_argument_name(i)
name_value = ifc_entity.wrapped_data.get_argument(i)
node[name]= name_value
node.__primarylabel__ = 'Root'
node.__primarykey__ = 'id'
return node
# Process literal attributes, entity attributes, and relationship attributes
def create_graph_from_ifc_entity_all(graph, ifc_entity, ifc_file):
node = create_pure_node_from_ifc_entity(ifc_entity, ifc_file)
graph.merge(node)
for i in range(ifc_entity.__len__()):
if ifc_entity[i]:
if ifc_entity.wrapped_data.get_argument_type(i) == 'ENTITY INSTANCE':
if ifc_entity[i].is_a() in ['IfcOwnerHistory'] and ifc_entity.is_a() != 'IfcProject':
continue
else:
sub_node = create_pure_node_from_ifc_entity(ifc_entity[i], ifc_file)
REL = Relationship(node, ifc_entity.wrapped_data.get_argument_name(i), sub_node)
graph.merge(REL)
elif ifc_entity.wrapped_data.get_argument_type(i) == 'AGGREGATE OF ENTITY INSTANCE':
for sub_entity in ifc_entity[i]:
sub_node = create_pure_node_from_ifc_entity(sub_entity, ifc_file)
REL = Relationship(node, ifc_entity.wrapped_data.get_argument_name(i), sub_node)
graph.merge(REL)
for rel_name in ifc_entity.wrapped_data.get_inverse_attribute_names():
if ifc_entity.wrapped_data.get_inverse(rel_name):
inverse_relations = ifc_entity.wrapped_data.get_inverse(rel_name)
for wrapped_rel_entity in inverse_relations:
rel_entity = ifc_file.by_id(wrapped_rel_entity.id())
sub_node = create_pure_node_from_ifc_entity(rel_entity, ifc_file)
REL = Relationship(node, rel_name, sub_node)
graph.merge(REL)
return
def create_full_graph(graph, ifc_file):
idx = 1
length = len(ifc_file.wrapped_data.entity_names())
for entity_id in ifc_file.wrapped_data.entity_names():
entity = ifc_file.by_id(entity_id)
print(idx, '/', length, entity)
create_graph_from_ifc_entity_all(graph, entity, ifc_file)
idx += 1
return
+120
View File
@@ -0,0 +1,120 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.templating import Jinja2Templates
from api import foundation
from api import bcf
from api import documents
endpoint_metadata = [
{"name": "api_versions_get", "description": "/foundation/versions"},
{"name": "foundation_auth_get", "description": "/foundation/1.0/auth"},
{"name": "authentication_get", "description": "/authentication"},
{"name": "login_for_access_token_post", "description": "/foundation/oauth2/token"},
{"name": "current_user_get", "description": "/foundation/1.0/current-user"},
{"name": "projects_get",
"description": "Retrieve a collection of projects that the currently logged on user has access to."},
{"name": "project_get",
"description": "Retrieve a specific project. The top level data container is known as the BCF project, "
"with a UUID and a project name attribute."},
{"name": "project_put",
"description": "Modify a specific project. This operation is only possible when the server returns the update "
"flag in the Project authorization."},
{"name": "project_extensions_get",
"description": "Retrieve a specific projects extensions. Project extensions are used to define possible values "
"that can be used in topics and comments, for example topic labels and priorities. They may "
"change during the course of a project. The most recent extensions state which values are valid "
"at a given moment for newly created topics and comments."},
{"name": "topics_get",
"description": "Retrieve a collection of topics related to a project (default sort order is creation_date)."},
{"name": "topic_post",
"description": "Add a new topic. The BCF project contains zero or more topics. Each topic represents a model "
"issue. A topic will have a UUID, a title, description, priority, stage, labels (similar to "
"tags), creation date / author, due date, and assigned to. If modified, it may contain the "
"modification date and author."},
{"name": "topic_get", "description": "Retrieve a specific topic."},
{"name": "topic_put", "description": "Modify a specific topic, description similar to POST."},
{"name": "bim_snippet_get",
"description": "Retrieves a topics BIM-Snippet as binary file. BIM snippet has been in BCF specification since "
"the very beginning, but is has never been used. Snippets have originally been added to provide "
"for 'changes in IFC'. To get this really working in a CDE environment changes to the IFC format "
"is necessary."},
{"name": "bim_snippet_put",
"description": "Puts a new BIM Snippet binary file to a topic. If this is used, the parent topics BIM Snippet "
"property is_external must be set to false and the reference must be the file name with "
"extension."},
{"name": "files_get", "description": "Retrieve a collection of file references as topic header."},
{"name": "files_put", "description": "Update a collection of file references on the topic header."},
{"name": "comments_get",
"description": "Retrieve a collection of all comments related to a topic (default ordering is date)."},
{"name": "comment_post", "description": "Add a new comment to a topic."},
{"name": "comment_put", "description": "Update a single comment, description similar to POST."},
{"name": "comment_get", "description": "Get a single comment."},
{"name": "viewpoints_get", "description": "Retrieve a collection of all viewpoints related to a topic."},
{"name": "viewpoint_post",
"description": "Add a new viewpoint. Viewpoints are immutable, meaning that they should never change. "
"Requirements for different visualizations should be handled by creating new viewpoint elements."},
{"name": "viewpoint_get", "description": "Retrieve a specific viewpoint."},
{"name": "viewpoint_selected_components_get",
"description": "Retrieve a collection of all selected components in a viewpoint."},
{"name": "viewpoint_colored_components_get",
"description": "Retrieve a collection of all colored components in a viewpoint."},
{"name": "viewpoint_components_visibility_get", "description": "Retrieve visibility of components in a viewpoint."},
{"name": "viewpoint_snapshot_get", "description": "Retrieve a specific viewpoints bitmap image file (png or jpg)."},
{"name": "related_topics_get", "description": "Retrieve a collection of all related topics to a topic."},
{"name": "related_topics_put", "description": "Add or update a collection of all related topics to a topic."},
{"name": "topic_document_references_get",
"description": "Retrieve a collection of all document references to a topic."},
{"name": "topic_document_references_post", "description": "Add or update document references to a topic."},
{"name": "topic_document_references_put", "description": "Add or update document references to a topic."},
{"name": "documents_get", "description": "Retrieve a collection of all documents uploaded to a project."},
{"name": "document_post", "description": "Upload a document (binary file) to a project."},
{"name": "document_get", "description": "Retrieves a document as binary file."},
{"name": "topics_events_get",
"description": "Retrieve a collection of topic events related to a project (default sort order is date)."},
{"name": "topic_events_get",
"description": "Retrieve a collection of topic events related to a project (default sort order is date)."},
{"name": "comments_events_get",
"description": "Retrieve a collection of comment events related to a project (default sort order is date)."},
{"name": "comment_events_get",
"description": "Retrieve a collection of comment events related to a comment (default sort order is date)."}
]
app = FastAPI(
title="Kontroll API",
description="Implementering av BCF API 3.0",
version="0.0.1",
openapi_tags=endpoint_metadata
)
# Configure app to accept requests from anywhere
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(foundation.router, prefix='')
app.include_router(bcf.router, prefix='')
app.include_router(documents.router, prefix='')
templates = Jinja2Templates(directory="templates")
@app.get("/", response_class=HTMLResponse)
def index(request: Request):
return templates.TemplateResponse(
"index.html",
{"request": request})
favicon_path = 'favicon.ico'
@app.get('/favicon.ico', include_in_schema=False)
async def favicon():
return FileResponse(favicon_path)
@@ -0,0 +1,87 @@
from pydantic import BaseModel
from typing import List, Optional
from enum import Enum
class BimSnippet(BaseModel):
snippet_type: str
is_external: str
reference: str
reference_schema: str
class Point(BaseModel):
x: Optional[float] = None
y: Optional[float] = None
z: Optional[float] = None
class Line(BaseModel):
start_point: Optional[Point] = None
end_point: Optional[Point] = None
class Direction(BaseModel):
x: Optional[float] = None
y: Optional[float] = None
z: Optional[float] = None
class OrthogonalCamera(BaseModel):
camera_view_point: Optional[Point] = None
camera_direction: Optional[Direction] = None
camera_up_vector: Optional[Direction] = None
view_to_world_scale: Optional[float] = None
aspect_ratio: Optional[float] = None
class PerspectiveCamera(BaseModel):
camera_view_point: Optional[Point] = None
camera_direction: Optional[Direction] = None
camera_up_vector: Optional[Direction] = None
field_of_view: Optional[float] = None
aspect_ratio: Optional[float] = None
class Location(BaseModel):
x: Optional[float] = None
y: Optional[float] = None
z: Optional[float] = None
class ClippingPlane(BaseModel):
location: Optional[Location] = None
direction: Optional[Direction] = None
class SnapshotType(Enum):
jpg = 'jpg'
png = 'png'
class BitmapType(Enum):
jpg = 'jpg'
png = 'png'
class Component(BaseModel):
ifc_guid: Optional[str] = None
originating_system: Optional[str] = None
authoring_tool_id: Optional[str] = None
class Coloring(BaseModel):
color: Optional[str] = None
components: Optional[List[Component]] = None
class ViewSetupHints(BaseModel):
spaces_visible: Optional[bool] = False
space_boundaries_visible: Optional[bool] = False
openings_visible: Optional[bool] = False
class Visibility(BaseModel):
default_visibility: Optional[bool] = False
exceptions: Optional[List[Component]] = None
view_setup_hints: Optional[ViewSetupHints] = None
@@ -0,0 +1,108 @@
from pydantic import BaseModel
from typing import List, Optional
from models.bcf_common import BimSnippet, BitmapType, Location, Direction, SnapshotType, Component
from models.bcf_common import Coloring, OrthogonalCamera, Visibility, PerspectiveCamera, Line, ClippingPlane
class ProjectPUT(BaseModel):
name: str
class TopicPOST(BaseModel):
guid: Optional[str] = None
topic_type: Optional[str] = None
topic_status: Optional[str] = None
reference_links: Optional[List[str]] = None
title: str
priority: Optional[str] = None
index: Optional[int] = None
labels: Optional[List[str]] = None
assigned_to: Optional[str] = None
stage: Optional[str] = None
description: Optional[str] = None
bim_snippet: Optional[BimSnippet] = None
due_date: Optional[str] = None
class TopicPUT(BaseModel):
topic_type: Optional[str] = None
topic_status: Optional[str] = None
reference_links: Optional[List[str]] = None
title: str
priority: Optional[str] = None
index: Optional[int] = None
labels: Optional[List[str]] = None
assigned_to: Optional[str] = None
stage: Optional[str] = None
description: Optional[str] = None
bim_snippet: Optional[BimSnippet] = None
due_date: Optional[str] = None
class FilePUT(BaseModel):
ifc_project: Optional[str] = None
ifc_spatial_structure_element: Optional[str] = None
filename: Optional[str] = None
date: Optional[str] = None
reference: Optional[str] = None
class CommentPOST(BaseModel):
guid: Optional[str] = None # comment id
comment: str
viewpoint_guid: Optional[str] = None
# reply_to_comment_guid: Optional[str] = None
class CommentPUT(BaseModel):
comment: str
viewpoint_guid: Optional[str] = None
class BitmapPOST(BaseModel):
bitmap_type: Optional[BitmapType] = None
bitmap_data: Optional[str] = None
location: Optional[Location] = None
normal: Optional[Direction] = None
up: Optional[Direction] = None
height: Optional[float] = None
class SnapshotPOST(BaseModel):
snapshot_type: Optional[SnapshotType] = None
snapshot_data: Optional[str] = None
class Components(BaseModel):
selection: Optional[List[Component]] = None
coloring: Optional[List[Coloring]] = None
visibility: Optional[Visibility] = None
class ViewpointPOST(BaseModel):
guid: Optional[str] = None
index: Optional[int] = None
orthogonal_camera: Optional[OrthogonalCamera] = None
perspective_camera: Optional[PerspectiveCamera] = None
lines: Optional[List[Line]] = None
clipping_planes: Optional[List[ClippingPlane]] = None
bitmaps: Optional[List[BitmapPOST]] = None
snapshot: Optional[SnapshotPOST] = None
components: Optional[Components] = None
class RelatedTopicPUT(BaseModel):
related_topic_guid: str
class DocumentReferencePOST(BaseModel):
guid: Optional[str] = None
document_guid: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
class DocumentReferencePUT(BaseModel):
document_guid: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
@@ -0,0 +1,197 @@
from models.bcf_common import *
class ProjectAction(Enum):
update = 'update'
createTopic = 'createTopic'
createDocument = 'createDocument'
class ProjectGETAuthorization(BaseModel):
project_actions: Optional[List[ProjectAction]] = None
class ProjectGET(BaseModel):
project_id: str
name: str
authorization: Optional[ProjectGETAuthorization] = None
class TopicAction(Enum):
update = 'update'
updateBimSnippet = 'updateBimSnippet'
updateRelatedTopics = 'updateRelatedTopics'
updateDocumentReferences = 'updateDocumentReferences'
updateFiles = 'updateFiles'
createComment = 'createComment'
createViewpoint = 'createViewpoint'
delete = 'delete'
class CommentAction(Enum):
update = 'update'
delete = 'delete'
class ExtensionsGET(BaseModel):
topic_type: List[str]
custom_information: List[str]
topic_status: List[str]
topic_label: List[str]
snippet_type: List[str]
priority: List[str]
users: List[str]
stage: List[str]
project_actions: Optional[List[str]] = None
topic_actions: Optional[List[str]] = None
comment_actions: Optional[List[str]] = None
class TopicGETAuthorization(BaseModel):
topic_actions: Optional[List[TopicAction]] = None
topic_status: Optional[List[str]] = None
class TopicGET(BaseModel):
guid: str
server_assigned_id: str
topic_type: Optional[str] = None
topic_status: Optional[str] = None
reference_links: Optional[List[str]] = None
title: str
priority: Optional[str] = None
index: Optional[int] = None
labels: Optional[List[str]] = None
creation_date: str
creation_author: str
modified_date: Optional[str] = None
modified_author: Optional[str] = None
assigned_to: Optional[str] = None
stage: Optional[str] = None
description: Optional[str] = None
bim_snippet: Optional[BimSnippet] = None
due_date: Optional[str] = None
authorization: Optional[TopicGETAuthorization] = None
class ProjectFileDisplayInformation(BaseModel):
field_display_name: str
field_value: str
class FileGET(BaseModel):
ifc_project: Optional[str] = None
ifc_spatial_structure_element: Optional[str] = None
filename: Optional[str] = None
date: Optional[str] = None
reference: Optional[str] = None
class ProjectFileInformation(BaseModel):
display_information: Optional[List[ProjectFileDisplayInformation]] = None
file: Optional[FileGET] = None
class CommentGETAuthorization(BaseModel):
comment_actions: Optional[List[CommentAction]] = None
class CommentGET(BaseModel):
guid: str
date: str
author: str
comment: str
topic_guid: str
viewpoint_guid: Optional[str] = None
reply_to_comment_guid: Optional[str] = None
modified_date: Optional[str] = None
modified_author: Optional[str] = None
authorization: Optional[CommentGETAuthorization] = None
class BitmapGET(BaseModel):
guid: Optional[str] = None
bitmap_type: Optional[BitmapType] = None
location: Optional[Location] = None
normal: Optional[Direction] = None
up: Optional[Direction] = None
height: Optional[float] = None
class SnapshotGET(BaseModel):
snapshot_type: Optional[SnapshotType] = None
class ViewpointAction(Enum):
delete = 'delete'
class ViewpointGETAuthorization(BaseModel):
viewpoint_actions: Optional[List[ViewpointAction]] = None
class ViewpointGET(BaseModel):
index: Optional[int] = None
guid: str
orthogonal_camera: Optional[OrthogonalCamera] = None
perspective_camera: Optional[PerspectiveCamera] = None
lines: Optional[List[Line]] = None
clipping_planes: Optional[List[ClippingPlane]] = None
bitmaps: Optional[List[BitmapGET]] = None
snapshot: Optional[SnapshotGET] = None
authorization: Optional[ViewpointGETAuthorization] = None
class ColoringGET(BaseModel):
coloring: Optional[List[Coloring]] = None
class SelectionGET(BaseModel):
selection: Optional[List[Component]] = None
class VisibilityGET(BaseModel):
visibility: Optional[Visibility] = None
class RelatedTopicGET(BaseModel):
related_topic_guid: str
class DocumentReferenceGET(BaseModel):
guid: str
document_guid: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
class DocumentGET(BaseModel):
guid: str
filename: str
class TopicEventGET(BaseModel):
topic_guid: str
date: str
author: str
# actions: Optional[List[EventAction]] = Field(None, min_items=1)
class CommentEventGET(BaseModel):
comment_guid: str
topic_guid: str
date: str
author: str
# actions: Optional[List[EventAction]] = Field(None, min_items=1)
# ---- maybe not necessary now
class EventAction(BaseModel):
type: str
value: Optional[str] = None
class Error(BaseModel):
message: str
@@ -0,0 +1,99 @@
from pydantic import BaseModel, Field, constr
from typing import List, Optional
# This file contains models used in both requests and responses during upload and download of documents.
# ---- REQUEST MODELS ---- #
class CallbackLink(BaseModel):
url: constr(min_length=1) = Field(
description='The server will web-browser-redirect to this URL once the user has completed selecting '
'documents or entering document metadata on the CDE'
)
expires_in: int = Field(
description='The expiry period for the URL, in seconds'
)
# ---- RESPONSE MODELS ---- #
class LinkData(BaseModel):
url: constr(min_length=1)
class DocumentVersionLinks(BaseModel):
document_version: LinkData
document_version_metadata: LinkData
document_version_download: LinkData
document_versions: LinkData
document_details: Optional[LinkData] = None
class FileDescription(BaseModel):
name: constr(min_length=1) = Field(
description='The name of the document version file on the server. The files are named by '
'document_id.file_ending',
example='908e1cd4-2e09-11ee-be56-0242ac120002.ifc'
)
size_in_bytes: int = Field(
description='The size of the file in bytes',
example='124563'
)
class Document(BaseModel):
document_id: constr(min_length=1) = Field(
description='A machine readable identifier that can be used to uniquely identify this version in future calls '
'UUID is used - see `Query` section',
example='908e1cd4-2e09-11ee-be56-0242ac120002'
)
session_file_id: Optional[str] = Field(
description='A machine readable identifier that can be used to uniquely the file '
'during the upload session, UUID is used',
example='908e1cd4-2e09-11ee-be56-0242ac120002'
)
version_index: int = Field(
description='A machine readable sequence number of the version of the document. The sequence must be ordered, '
'so that newer versions have higher values than previous ones. Each version index must be unique '
'for that document, but there may be gaps in the sequence',
example='12'
)
version_number: Optional[constr(min_length=1)] = Field(
description='A human readable version number. This is not expected to be in any specific format across CDEs '
'and may hold any value',
example='V2.0-larger'
)
creation_date: str = Field(
description='The creation date of the document revision',
example='2016-04-28T16:31:12.270+02:00'
)
title: Optional[constr(min_length=1)] = Field(
description='A human readable code or identifier. Metadata entered by user in CDE.',
example='Large garage'
)
original_file_name: Optional[str] = Field(
description='The full name of the file as sent to the API',
example='First_floor_vent.ifc')
file_ending: Optional[str] = Field(
description='The ending of the file name, including the dot',
example='.ifc')
mime_type: Optional[str] = Field(
description='The mime type identifier',
example='application/x-step')
file_type: Optional[str] = Field(
description='The full name of the file type',
example='STEP Physical File (SPF)')
project: Optional[str] = Field(
description='The project to which the document will belong once it has been uploaded,'
'this information is added as metadata by the user in the CDE',
example='908e1cd4-2e09-11ee-be56-0242ac120003')
file_description: FileDescription
parts: Optional[List[str]]
class DocumentVersion(Document):
links: DocumentVersionLinks
@@ -0,0 +1,26 @@
from typing import List, Optional
from pydantic import BaseModel
from models.documents_common import Document, DocumentVersion
# This file contains models that are not used at all, at least not yet.
# ---- not used at all, at least not yet ----
class DocumentQuery(BaseModel):
document_ids: List[str]
class DocumentUpload(Document):
session_file_id: Optional[str]
ifc_project: Optional[str]
class MetadataForDocumentsSaved(BaseModel):
documents: Optional[List[str]]
class DocumentQueryResult(BaseModel):
versions: List[DocumentVersion]
@@ -0,0 +1,84 @@
from pydantic import BaseModel, Field
from typing import List, Optional
from models.documents_common import CallbackLink, Document
from models.documents_request import FileToUpload, DocumentMetadataEntry
from models.request import User
# This file contains models that are neither used in requests nor in responses,
# and they can be used during both upload and download of documents.
# ---- UPLOAD MODELS ----
class ProjectOnly(BaseModel):
project_id: str
name: str
class DataForUploadDocuments(BaseModel):
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
documents: List[FileToUpload]
callback: Optional[CallbackLink]
current_user: Optional[User]
projects: List[ProjectOnly]
# ---- DOWNLOAD MODELS ----
class DocumentMetadataEntries(BaseModel):
metadata: List[DocumentMetadataEntry] = Field(
description='An array of metadata entries'
)
class Project(BaseModel):
project_id: str
name: str
documents: Optional[List[Document]] = Field(
description='An array containing all the documents selected by the user'
)
class DataForDocumentSelection(BaseModel):
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
projects: List[Project]
callback: Optional[CallbackLink]
current_user: Optional[User]
# ---- OTHER MODELS ----
file_types = {
'smc': {
'file_type': 'Solibri Model Checker',
'file_ending': '.smc',
'mime_type': 'application/octet-stream'
},
'ifc': {
'file_type': 'STEP Physical File',
'file_ending': '.ifc',
'mime_type': 'application/x-step'
},
'ifczip': {
'file_type': 'ZIP of a STEP Physical File',
'file_ending': '.ifcZIP',
'mime_type': 'application/zip'
},
'pdf': {
'file_type': 'Adobe Portable Document Format',
'file_ending': '.pdf',
'mime_type': 'application/pdf'
}
}
@@ -0,0 +1,92 @@
from pydantic import BaseModel, Field, constr
from typing import List, Optional
from enum import Enum
from models.documents_common import CallbackLink
# This file contains models used in requests during upload and download of documents.
# ---- UPLOAD MODELS ----
class FileToUpload(BaseModel):
file_name: constr(min_length=1) = Field(
description='The CDE UI will display this value to the User when entering document metadata. This is the '
'original name of the file. This attribute is the same as the name attribute in a document model.'
)
session_file_id: constr(min_length=1) = Field(
description='This is a client provided id to differentiate between multiple files that are being uploaded in '
'the same session'
)
document_id: Optional[constr(min_length=1)] = Field(
description='When present, indicates that this upload is a new version of an existing document'
)
class UploadDocuments(BaseModel):
callback: CallbackLink
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
files: List[FileToUpload]
class UploadFileDetail(BaseModel):
size_in_bytes: int = Field(
description='The uploaded file size'
)
session_file_id: constr(min_length=1) = Field(
description='This is a client provided id to differentiate between multiple files that are being uploaded in '
'the same session'
)
class UploadFileDetails(BaseModel):
files: List[UploadFileDetail]
# ---- DOWNLOAD MODELS ----
class SelectDocuments(BaseModel):
callback: CallbackLink
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
supported_file_extensions: Optional[List[str]] = Field(
description='The client may optionally provide an array of accepted file extensions that should be opened '
'during this flow. The CDE server UI should make an attempt to only show files matching these '
'extensions to the user for the download selection or help the user in selecting the desired '
'files. However, the server does not have to guarantee that only files matching the extensions '
'will be selected. The extensions here must contain the dot separator.',
example=['.ifc', '.ifczip']
)
class DataType(Enum):
string = 'string'
boolean = 'boolean'
date_time = 'date-time'
date = 'date'
integer32 = 'integer32'
integer64 = 'integer64'
number = 'number'
url = 'url'
class DocumentMetadataEntry(BaseModel):
name: constr(min_length=1) = Field(
description='The name of the metadata property'
)
value: List[constr(min_length=1)] = Field(
description='The value of the metadata property, can be a list'
)
data_type: DataType = Field(
description='The data type of the items in the value array'
)
@@ -0,0 +1,139 @@
from pydantic import BaseModel, Field, constr
from typing import List, Optional
from enum import Enum
from models.documents_common import DocumentVersion, LinkData
# This file contains models used in responses during upload and download of documents.
# ---- UPLOAD MODELS ----
class DocumentUploadSessionInitialization(BaseModel):
upload_ui_url: constr(min_length=1) = Field(
description='A CDE UI URL for the client to open in a local browser. The user would enter document metadata '
'directly in the CDE'
)
expires_in: int = Field(
description='`upload_ui_url` expiry in seconds'
)
max_size_in_bytes: int = Field(
description='The maximum file size supported by the CDE. Attempts to upload a larger file will fail'
)
class HttpMethod(Enum):
POST = 'POST'
PUT = 'PUT'
class HeaderValue(BaseModel):
name: constr(min_length=1)
value: constr(min_length=1)
class Headers(BaseModel):
values: List[HeaderValue]
class MultipartFormData(BaseModel):
prefix: str = Field(
description='This is a server provided value. Its value must be prefixed to the binary content body when '
'uploading this part'
)
suffix: str = Field(
description='This is a server provided value. Its value must be suffixed to the binary content body when '
'uploading this part. Typically, this is the end boundary for a multipart/form-data request'
)
class UploadFilePartInstruction(BaseModel):
url: constr(min_length=1)
http_method: HttpMethod
additional_headers: Optional[Headers] = None
include_authorization: Optional[bool] = Field(
description='Whether or not to include the authorization request header in the file upload request. '
'Including the authorization header with some cloud storage providers might fail the request'
)
multipart_form_data: Optional[MultipartFormData] = None
content_range_start: int = Field(
description='The inclusive, zero index based start for this part'
)
content_range_end: int = Field(
description='The inclusive, zero index based end for this part'
)
class DocumentToUpload(BaseModel):
session_file_id: constr(min_length=1) = Field(
description='A client-provided identifier that allows matching the specification with the correct file on the '
"user's machine"
)
upload_file_parts: List[UploadFilePartInstruction] = Field(
description='An array of request specifications detailing how to split the file to parts and upload each part '
'to the CDE'
# min_length=1,
)
upload_completion: LinkData
upload_cancellation: LinkData
class DocumentsToUpload(BaseModel):
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
documents_to_upload: Optional[List[DocumentToUpload]]
# ---- DOWNLOAD MODELS ----
class DocumentDiscoverySessionInitialization(BaseModel):
select_documents_url: constr(min_length=1) = Field(
description='A CDE UI URL for the client to open in a local browser. The user would search and select '
'documents directly in the CDE'
)
expires_in: int = Field(
description='`select_documents_url` expiry in seconds'
)
class DocumentsMarkedAsSelected(BaseModel):
documents: Optional[List[str]]
class SelectedDocuments(BaseModel):
server_context: Optional[str] = Field(
description="A CDE controlled identifier recording the user's context on the CDE. For example which project "
'and folder the user was on. If the client provides the `server_context` in subsequent calls then '
'the CDE will attemp to load the UI at the same place.'
)
documents: List[DocumentVersion] = Field(
description='An array containing all the documents selected by the user'
)
class DocumentMetadata(BaseModel):
session_file_id: constr(min_length=1) = Field(
description='This is a client provided id to differentiate between multiple files that are being uploaded in '
'the same session'
)
document_id: Optional[constr(min_length=1)] = Field(
description='When present, indicates that this upload is a new version of an existing document'
)
version_number: constr(min_length=1) = Field(
description='A human readable version number. This is not expected to be in any specific format across CDEs '
'and may hold any value'
)
title: constr(min_length=1) = Field(
description='A human readable code or identifier'
)
project: Optional[str]
class DocumentVersions(BaseModel):
documents: List[DocumentVersion]
@@ -0,0 +1,17 @@
from pydantic import BaseModel
class TokenRequest(BaseModel):
grant_type: str | None = None
class UserInfo(BaseModel):
username: str
scope: str
class TokenInfo(BaseModel):
access_token: str | None = None
refresh_token: str | None = None
token_type: str | None = None
expires_in: str | None = None
+18
View File
@@ -0,0 +1,18 @@
from pydantic import BaseModel
from typing import List
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
scopes: List[str] = []
expires: str | None = None
class LoginReq(BaseModel):
username: str
password: str
@@ -0,0 +1,20 @@
from pydantic import BaseModel
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class Config:
orm_mode = True
allow_population_by_field_name = True
class UserInDB(User):
hashed_password: str
class Config:
orm_mode = True
allow_population_by_field_name = True
@@ -0,0 +1,4 @@
from pydantic import BaseModel
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,186 @@
import os
import hashlib
from datetime import timedelta
from fastapi import HTTPException
from database.neo4j import MyDB, driver
from security.secure import create_access_token
from jose import jwt
from security.secrets import get_secrets
from security.secure import credentials_exception
from models.foundation_other import *
from models.other import *
secrets = get_secrets()
class FoundationDB(MyDB):
# implemented
def create_authorization_code(self, username, authorization_code, scope) -> bool:
def create_authorization_code_work(tx) -> bool:
cypher = """
MATCH (u:User)
WHERE u.username = $username
OPTIONAL MATCH (u)-[r1:CAN_USE]->(t:Token)
WITH u, t
DETACH DELETE t
MERGE (u)-[r:CAN_USE]->(ac:AuthorizationCode)
SET ac.code = $authorization_code
SET ac.scope = $scope
WITH ac
CALL apoc.ttl.expireIn(ac, $time_delta, 's')
RETURN ac AS authorization_code
"""
result = tx.run(cypher,
username=username,
authorization_code=authorization_code,
scope=scope,
time_delta=int(os.environ['SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS']))
summary = result.consume()
if summary.counters.nodes_created < 1:
raise HTTPException(status_code=400, detail="Authorization code was not created.")
return summary.counters.nodes_created
with self.driver.session() as session:
return session.execute_write(create_authorization_code_work)
def use_authorization_code(self, authorization_code) -> TokenInfo:
def use_code_to_get_user_info_work(tx) -> TokenData:
cypher = """
MATCH (u:User)-[r1:CAN_USE]->(ac:AuthorizationCode)
WHERE ac.code = $authorization_code
WITH u.username AS username, ac.scope AS scope
RETURN
username, scope
"""
result = tx.run(cypher,
authorization_code=authorization_code)
first = result.single()
if first is None:
raise HTTPException(status_code=404, detail="Authorization code not found.")
authorized_user = TokenData()
authorized_user.username = first.get("username")
authorized_user.scopes = first.get("scope").split(' ')
return authorized_user
with self.driver.session() as session:
user_info = session.execute_read(use_code_to_get_user_info_work)
token_info = TokenInfo()
token_info.access_token = create_access_token(
user_info.dict(),
timedelta(seconds=int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS'])))
token_info.refresh_token = create_access_token(
user_info.dict(),
timedelta(seconds=int(os.environ['SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS'])))
def add_tokens_and_delete_code_work(tx) -> bool:
cypher = """
MATCH (u:User)-[r1:CAN_USE]->(ac1:AuthorizationCode)
WHERE ac1.code = $authorization_code
AND u.username = $username
DETACH DELETE ac1
MERGE (u)-[r4:CAN_USE]->(t2:Token:AccessToken)
SET t2.value = $access_token
SET t2.hash = $access_token_hash
MERGE (u)-[r5:CAN_USE]->(t3:Token:RefreshToken)
SET t3.value = $refresh_token
SET t3.hash = $refresh_token_hash
"""
result = tx.run(cypher,
authorization_code=authorization_code,
username=user_info.username,
access_token=token_info.access_token,
access_token_hash=hashlib.md5(token_info.access_token.encode('utf-8')).hexdigest(),
refresh_token=token_info.refresh_token,
refresh_token_hash=hashlib.md5(token_info.refresh_token.encode('utf-8')).hexdigest())
summary = result.consume()
if summary.counters.nodes_created < 1 or summary.counters.nodes_deleted < 1:
raise HTTPException(status_code=400, detail="Tokens were not created.")
return summary.counters.nodes_created
with self.driver.session() as session:
session.execute_write(add_tokens_and_delete_code_work)
return token_info
def use_refresh_token(self, refresh_token) -> TokenInfo:
def use_refresh_to_get_access_work(tx) -> (TokenData, TokenInfo):
cypher = """
MATCH (rt:RefreshToken)<-[r1:CAN_USE]-(u:User)-[r2:CAN_USE]->(at:AccessToken)
WHERE u.username = $username
AND rt.hash = $refresh_token_hash
RETURN
at.value AS access_token
"""
refresh_token_payload = jwt.decode(refresh_token,
secrets['security_secret_key'],
algorithms=[os.environ['SECURITY_ALGORITHM']])
username_from_refresh_token: str = refresh_token_payload.get("username")
print('refresh_token_username: ', username_from_refresh_token)
result = tx.run(cypher,
username=username_from_refresh_token,
refresh_token_hash=hashlib.md5(refresh_token.encode('utf-8')).hexdigest()
)
first = result.single()
if first is None:
raise HTTPException(status_code=404, detail="Access token not found.")
token_info = TokenInfo()
token_info.access_token = first.get("access_token")
token_info.refresh_token = refresh_token
access_token_payload = jwt.decode(token_info.access_token,
secrets['security_secret_key'],
algorithms=[os.environ['SECURITY_ALGORITHM']])
username_from_access_token: str = access_token_payload.get("username")
if username_from_access_token is None:
raise credentials_exception
token_scopes = access_token_payload.get("scopes", [])
token_data = TokenData(scopes=token_scopes, username=username_from_access_token)
return token_data, token_info
with self.driver.session() as session:
got_token_data, got_token_info = session.execute_read(use_refresh_to_get_access_work)
new_token_info = TokenInfo()
new_token_info.access_token = create_access_token(
got_token_data.dict(),
timedelta(seconds=int(os.environ['SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS'])))
new_token_info.refresh_token = create_access_token(
got_token_data.dict(),
timedelta(seconds=int(os.environ['SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS'])))
def update_tokens_work(tx) -> bool:
cypher = """
MATCH (u:User)
WHERE u.username = $username
OPTIONAL MATCH (u)-[r1:CAN_USE]->(t:Token)
WITH u, t
DETACH DELETE t
MERGE (rt:Token:RefreshToken)<-[r2:CAN_USE]-(u)-[r3:CAN_USE]->(at:Token:AccessToken)
SET
rt.value = $refresh_token,
at.value = $access_token,
rt.hash = $refresh_token_hash,
at.hash = $access_token_hash
"""
result = tx.run(cypher,
username=got_token_data.username,
access_token=new_token_info.access_token,
refresh_token=new_token_info.refresh_token,
access_token_hash=hashlib.md5(new_token_info.access_token.encode('utf-8')).hexdigest(),
refresh_token_hash=hashlib.md5(new_token_info.refresh_token.encode('utf-8')).hexdigest())
summary = result.consume()
if summary.counters.nodes_created < 2 or summary.counters.nodes_deleted < 2:
raise HTTPException(status_code=400, detail="Tokens were not deleted and created.")
return summary.counters.nodes_created
with self.driver.session() as session:
session.execute_write(update_tokens_work)
return new_token_info
foundation_db = FoundationDB(driver)
@@ -0,0 +1,10 @@
fastapi==0.101.0
httpx==0.24.1
jose==1.0.0
jsonpickle==3.0.1
passlib==1.7.4
pydantic==1.10.7
python_dateutil==2.8.2
python_jose==3.3.0
py2neo==2021.2.4
@@ -0,0 +1,10 @@
from glob import glob
def get_secrets():
secrets = dict()
for var in glob('/run/secrets/*'):
k = var.split('/')[-1]
v = open(var).read().rstrip('\n')
secrets[k] = v
return secrets
@@ -0,0 +1,117 @@
from fastapi import Depends, HTTPException, status, Security
from fastapi.security import OAuth2AuthorizationCodeBearer, SecurityScopes
from datetime import datetime, timedelta
from passlib.context import CryptContext
from jose import jwt, JWTError
from models.other import TokenData
from models.request import User
from database.neo4j import db
import os
from security.secrets import get_secrets
secrets = get_secrets()
# password context
crypt_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto")
oauth2_scheme = OAuth2AuthorizationCodeBearer(
authorizationUrl='foundation/oauth2/auth',
tokenUrl='foundation/oauth2/token',
scopes={
'test': 'Full access, but only test data.',
'user': 'Normal user access.',
'admin': 'Full access to all.'
})
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
def create_access_token(data: dict, expires_delta: timedelta | None = None):
payload = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
payload.update({"expires": str(expire)})
encoded_jwt = jwt.encode(payload, secrets['security_secret_key'], algorithm=os.environ['SECURITY_ALGORITHM'])
return encoded_jwt
def verify_password(plain_password, hashed_password):
return crypt_context.verify(plain_password, hashed_password)
# see if given password matches password of username
def authenticate_user(username_to_authenticate: str, plain_password: str) -> dict or False:
# maybe put an exception around this code below
user = db.get_user(username_to_authenticate)
if not user:
return False
if verify_password(plain_password, user.hashed_password):
return user
return False
def get_password_hash(plain_password):
return crypt_context.hash(plain_password)
# get current user from token
async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
print("\n\n\nGets current user.")
if security_scopes.scopes:
authenticate_value = f'Bearer scope="{security_scopes.scope_str}"'
else:
authenticate_value = f"Bearer"
print(authenticate_value)
try:
print('Token: ', token)
payload = jwt.decode(token,
secrets['security_secret_key'],
algorithms=[os.environ['SECURITY_ALGORITHM']])
username_from_token: str = payload.get("username")
print('Token username: ', username_from_token)
if username_from_token is None:
raise credentials_exception
token_scopes = payload.get("scopes", [])
print('Token scopes: ', token_scopes)
token_data = TokenData(scopes=token_scopes, username=username_from_token)
except JWTError:
print('JWTError')
raise credentials_exception
user = db.get_user(username=token_data.username)
if user is None:
raise credentials_exception
for scope in security_scopes.scopes:
if scope not in token_data.scopes:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not enough permissions",
headers={"WWW-Authenticate": authenticate_value},
)
return user
async def get_current_active_user(current_user: User = Security(get_current_user, scopes=["test"])):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kontroll.digital</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script>
$(document).ready(function () {
$('#signin_button').on('click', function (event) {
event.preventDefault();
get_code();
});
});
function get_code () {
let form_values = $('#signin_form').serialize();
console.log(form_values);
$.ajax ({
type: 'GET',
url: './code',
data: form_values,
dataType: 'json',
success: function (msg) {
if (msg) {
console.log('Result: New authorization code: ' + msg);
send_code(msg)
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
function send_code (code) {
let redirect_uri = $('#redirect_uri').val();
let response_type = $('#response_type').val();
let scope = $('#scope').val();
let state = $('#state').val();
console.log('Redirect URI: ' + redirect_uri);
console.log('Response type: ' + response_type);
console.log('Scope: ' + scope);
console.log('State: ' + state);
console.log('Code: ' + code);
$.get ({
url: redirect_uri,
crossDomain: true,
data: {
response_type: response_type,
scope: scope,
state: state,
code: code
},
dataType: 'html',
success: function (msg) {
if (msg) {
console.log('Result text: ' + msg);
$('#result_message').text('Received verification code. You may now close this window.');
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
</script>
<style>
body {
align-content: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.page_layout {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="page_layout" class="page_layout">
<form action="./code" id="signin_form" name="signin_form">
<h1 class="h3 mb-3">kontroll.digital</h1>
<div id="result_message">
<label for="username" class="sr-only mb-1">Username</label>
<input type="text" id="username" name="username" class="form-control mb-3" placeholder="Username" required autofocus>
<label for="password" class="sr-only mb-1">Password</label>
<input type="password" id="password" name="password" class="form-control mb-3" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
By signing in, I allow: {{client_name}}, to access data from Kontroll BCF server, on my behalf.
</label>
</div>
<input type="hidden" id="response_type" name="response_type" value="{{response_type}}">
<input type="hidden" id="client_id" name="client_id" value="{{client_id}}">
<input type="hidden" id="client_name" name="client_name" value="{{client_name}}">
<input type="hidden" id="state" name="state" value="{{state}}">
<input type="hidden" id="redirect_uri" name="redirect_uri" value="{{redirect_uri}}">
<input type="hidden" id="scope" name="scope" value="{{scope}}">
<button class="btn btn-lg btn-primary btn-block" type="submit" id="signin_button">Sign in</button>
</div>
</form>
</div>
</body>
</html>
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kontroll.digital</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script>
$(document).ready(function () {
$('#signin_button').on('click', function (event) {
event.preventDefault();
get_code();
});
});
function get_code () {
let form_values = $('#signin_form').serialize();
console.log(form_values);
$.ajax ({
type: 'GET',
url: './code',
data: form_values,
dataType: 'json',
success: function (msg) {
if (msg) {
console.log('Result code: ' + msg);
send_code(msg)
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
function send_code (code) {
let redirect_uri = $('#redirect_uri').val();
let response_type = $('#response_type').val();
let scope = $('#scope').val();
let state = $('#state').val();
console.log('Redirect URI: ' + redirect_uri);
console.log('Response type: ' + response_type);
console.log('Scope: ' + scope);
console.log('State: ' + state);
console.log('Code: ' + code);
$.get ({
url: redirect_uri,
crossDomain: true,
data: {
response_type: response_type,
scope: scope,
state: state,
code: code
},
dataType: 'html',
success: function (msg) {
if (msg) {
console.log('Result text: ' + msg);
$('#result_message').text('Received verification code. You may now close this window.');
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
</script>
<style>
body {
align-content: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.page_layout {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="page_layout" class="page_layout">
<form action="./code" id="signin_form" name="signin_form">
<h1 class="h3 mb-3">kontroll.digital</h1>
<div id="result_message">
<label for="username" class="sr-only mb-1">Username</label>
<input type="text" id="username" name="username" class="form-control mb-3" placeholder="Username" required autofocus>
<label for="password" class="sr-only mb-1">Password</label>
<input type="password" id="password" name="password" class="form-control mb-3" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
By signing in, I allow: {{client_name}}, to access data from Kontroll BCF server, on my behalf.
</label>
</div>
<input type="hidden" id="response_type" name="response_type" value="{{response_type}}">
<input type="hidden" id="client_id" name="client_id" value="{{client_id}}">
<input type="hidden" id="client_name" name="client_name" value="{{client_name}}">
<input type="hidden" id="state" name="state" value="{{state}}">
<input type="hidden" id="scope" name="scope" value="{{scope}}">
<input type="hidden" id="redirect_uri" name="redirect_uri" value="{{redirect_uri}}">
<button class="btn btn-lg btn-primary btn-block" type="submit" id="signin_button">Sign in</button>
</div>
</form>
</div>
</body>
</html>
@@ -0,0 +1,225 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kontroll.digital</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script>
$(document).ready(function () {
// Text format functions
$('.size_in_bytes').each(function(i, obj) {
$(this).text(formatBytes($(this).text()));
console.log($(this).text());
});
$('.date_in_iso').each(function(i, obj) {
$(this).text(formatDate($(this).text()));
console.log($(this).text());
});
// Event functions
{% for project in projects %}
$('#button_{{project.project_id}}').on('click', function (event) {
let form_data = new FormData();
let files = $('#file_{{project.project_id}}')[0].files;
if(files.length > 0) {
form_data.append('file', files[0]);
form_data.append('project', '{{project.project_id}}');
form_data.append('selection_session', $('#selection_session').val());
$.ajax({
url: './upload_file_to_project',
type: 'POST',
data: form_data,
dataType: 'json',
contentType: false,
processData: false,
success:function (response) {
if (response.document_id) {
$('#span_{{project.project_id}}').text(response.document_id);
} else {
$('#span_{{project.project_id}}').text('Not uploaded');
}
}
});
} else {
$('#span_{{project.project_id}}').text('Please select a file.')
}
event.preventDefault();
});
{% endfor %}
$('#select_documents_button').on('click', function (event) {
select_documents();
})
});
// Download functions
function select_documents () {
event.preventDefault();
let form_values = $('#select_documents_form').serialize();
console.log('Form values: ' + form_values);
$.ajax ({
type: 'POST',
url: './mark-documents-as-selected',
data: form_values,
dataType: 'json',
success: function (msg) {
if (msg) {
console.log('Result of mark-some-documents-as-selected: ' + msg);
callback(msg)
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
return false;
}
function callback (msg) {
let callback_url = $('#callback_url').val();
let session_id = $('#selection_session').val();
let server_context = $('#server_context').val();
let selected_documents_url = 'https://api.kontroll.digital/documents/1.0/download-instructions'
+ '?session_id=' + session_id
+ '&server_context=' + server_context
callback_url = callback_url + '?selected_documents_url=' + encodeURIComponent(selected_documents_url)
console.log('Selected documents url: ' + selected_documents_url);
console.log('Callback url: ' + callback_url);
$.get ({
url: callback_url,
crossDomain: true,
dataType: 'html',
success: function (msg) {
if (msg) {
console.log('Result text: ' + msg);
$('#result_message').text('You may now close this window.');
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
// Helper functions
function formatBytes(bytes, decimals = 2) {
bytes = parseInt(bytes)
const k = 1024
if (!+bytes) return '0 Bytes'
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'Kb', 'Mb', 'Gb', 'Tb', 'Pb', 'Eb', 'Zb', 'Yb']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
function formatDate(iso_date) {
let d = new Date(iso_date)
let curr_date = d.getDate();
let curr_month = d.getMonth() + 1;
let curr_year = d.getFullYear();
return curr_year + "-" + curr_month + "-" + curr_date
}
</script>
<style>
body {
align-content: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.page_layout {
width: 100%;
max-width: 780px;
padding: 15px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="page_layout" class="page_layout">
<form action="" id="select_documents_form" name="select_documents_form">
<h1 class="h3 mb-3">kontroll.digital</h1>
<div id="select_documents_div">
<table class="table">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Title</th>
<th scope="col">Version</th>
<th scope="col">File type</th>
<th scope="col">Size</th>
<th scope="col">Created</th>
</tr>
</thead>
<tbody>
{% for project in projects %}
<tr>
<td colspan="7" class="project_upload">
<h2>{{project.name}}</h2>
<span>
<input type="file" id="file_{{project.project_id}}" name="file_{{project.project_id}}" />
<input type="button" id="button_{{project.project_id}}" name="button_{{project.project_id}}" value="Upload">
</span>
<span id="span_{{project.project_id}}"></span>
</td>
<tr>
{% for document in project.documents %}
<tr>
<td><input type="checkbox" id="document_{{document.document_id}}" name="document_{{document.document_id}}"></td>
<td><span>{{document.document_id}}</span></td>
<td><span>{{document.title}}</span></td>
<td><span>{{document.version_number}}</span></td>
<td><span>{{document.file_type}}</span></td>
<td><span class="size_in_bytes">{{document.file_description.size_in_bytes}}</span></td>
<td><span>{{document.creation_date}}</span></td>
<tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
<input type="hidden" id="username" name="username" value="{{current_user}}">
<input type="hidden" id="selection_session" name="selection_session" value="{{selection_session}}">
<input type="hidden" id="server_context" name="server_context" value="{{server_context}}">
<input type="hidden" id="callback_url" name="callback_url" value="{{callback_url}}">
<input type="hidden" id="callback_expires_in" name="callback_expires_in" value="{{callback_expires_in}}">
<button class="btn btn-lg btn-primary btn-block" id="select_documents_button" type="button">Select documents</button>
</div>
</form>
</div>
</body>
</html>
@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kontroll.digital</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.3.min.js"></script>
<script>
$(document).ready(function () {
$('#save_metadata_button').on('click', function (event) {
event.preventDefault();
save_metadata();
});
});
function save_metadata () {
let form_values = $('#save_metadata_form').serialize();
console.log('Form values: ' + form_values);
$.ajax ({
type: 'POST',
url: './save-metadata-for-documents',
data: form_values,
dataType: 'json',
success: function (msg) {
if (msg) {
console.log('Result of save-metadata-for-documents: ' + msg);
callback(msg)
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
function callback (msg) {
let callback_url = $('#callback_url').val();
let session_id = $('#upload_session').val();
let server_context = $('#server_context').val();
let upload_documents_url = 'https://api.kontroll.digital/documents/1.0/upload-instructions'
+ '?session_id=' + session_id
+ '&server_context=' + server_context
callback_url = callback_url + '?upload_documents_url=' + encodeURIComponent(upload_documents_url)
console.log('Upload documents url: ' + upload_documents_url);
console.log('Callback url: ' + callback_url);
$.get ({
url: callback_url,
crossDomain: true,
dataType: 'html',
success: function (msg) {
if (msg) {
console.log('Result text: ' + msg);
$('#result_message').text('You may now close this window.');
}
},
error: function () {
let errors = ['An unknown error occured.'];
console.log(errors);
}
});
}
</script>
<style>
body {
align-content: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.page_layout {
width: 100%;
max-width: 780px;
padding: 15px;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="page_layout" class="page_layout">
<form action="" id="save_metadata_form" name="save_metadata_form">
<h1 class="h3 mb-3">kontroll.digital</h1>
<div id="save_metadata_div">
<table class="table">
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Title</th>
<th scope="col">Version</th>
<th scope="col">File name</th>
</tr>
</thead>
<tbody>
{% for document in documents %}
<tr>
<input type="hidden" id="session_file_id@{{document.session_file_id}}" name="session_file_id@{{document.session_file_id}}" value="{{document.session_file_id}}">
<td>{{document.document_id}}<input type="hidden" id="document@{{document.session_file_id}}" name="document@{{document.session_file_id}}" value="{{document.document_id}}"></td>
<td><input type="text" id="title@{{document.session_file_id}}" name="title@{{document.session_file_id}}" value="{{document.title}}"></td>
<td><input type="text" id="version_number@{{document.session_file_id}}" name="version_number@{{document.session_file_id}}" value="{{document.version_number}}"></td>
<td>{{document.file_name}}<input type="hidden" id="filename@{{document.session_file_id}}" name="filename@{{document.session_file_id}}" value="{{document.file_name}}"></td>
<tr>
{% endfor %}
</tbody>
</table>
<input type="hidden" id="username" name="username" value="{{username}}">
<input type="hidden" id="email" name="email" value="{{email}}">
<input type="hidden" id="full_name" name="full_name" value="{{full_name}}">
<input type="hidden" id="upload_session" name="upload_session" value="{{upload_session}}">
<input type="hidden" id="server_context" name="server_context" value="{{server_context}}">
<input type="hidden" id="callback_url" name="callback_url" value="{{callback_url}}">
<input type="hidden" id="callback_expires_in" name="callback_expires_in" value="{{callback_expires_in}}">
<div class="form-group mb-3">
<label for="project">Associate document with this project:</label>
<select class="form-control" id="project" name="project">
{% for project in projects %}
<option value="{{project.project_id}}">{{project.name}}</option>
{% endfor %}
</select>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" id="save_metadata_button">Save metadata</button>
</div>
</form>
</div>
</body>
</html>