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
parent 10ef4293f3
commit 7ee66de043
124 changed files with 9066 additions and 3388 deletions
+12
View File
@@ -0,0 +1,12 @@
/api/app/logs/*
/api/app/favicon.ico
/data/
/etc/
/neo4j/data/*
/neo4j/import/*
/neo4j/logs/*
/secrets/*
/TODO.txt
/venv/
/.idea/
/secrets/
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
+268 -20
View File
@@ -1,27 +1,275 @@
# Server-Test
# Kontroll BCF API server - OpenCDE APIs
```
$ pip install -r requirements.txt
$ cd bcfserver/
$ python
>>> from run import db
>>> db.create_all()
$ export FLASK_APP=run.py
$ flask run
```
## What is this?
Go to [http://localhost:5000](http://localhost:5000) to see the server
OpenCDE APIs:
Foundation API,
BCF API,
Documents API
implemented in python,
the FastAPI framework
and the Neo4j graph database.
# Register the user
This is server software. To run or test this code you need to set up your server to work with a client.
This server was developed and tested using Solibri as a client. To use a client with this server you need to configure
a client id and a client key.
1. Go to http://localhost:5000/register to register the user
2. Create the client
3. For grant type enter authorization_code
4. For response_type enter code secret
5. Enter the scope and create the client
You also need to configure encrypted communication and authentication using HTTP and OAuth2.
And then you need an SSL-certificate. To get an SSL-certificate you need your own domain name.
Solibri can only connect to preconfigured domains.
### You will be redirected to the page with the details of your client id and secret
This means that it is not so easy to test this software.
In the future I hope an open source BCF/CDE-client can be developed to access this server.
Maybe Blender BIM in the future could be extended with such BCF/CDE-client functionality.
# Foundation API
## Background
- Set the Base URL will be `http://127.0.0.1:5000/`
You can read some background information about the project here:
https://martin.wiss.se/data/bcf_ifc_graph.pdf
The name of this project is Kontroll BCF API server because it was originally
developed as a POC prototype to test if BCF API could be used for digital inspection plans
for building permits (translated as "kontroll-plan" in Swedish language).
## Code structure
/docker-compose.yml
You bring up the project using this file.
/api/Dockerfile
Contains instructions for the FastAPI container.
Referenced by /docker-compose.yml
/api/app
Contains the server application.
/api/app/main.py
This is where the application starts.
The FastAPI-container will run this script on uvicorn.
main.py will load routes from scripts in the /api/app/api folder.
/api/app/api
This is where the FastAPI routes are defined.
/api/app/repository
Routes often runs methods that will read or write data to or from a database.
This is where methods that read or write data from database are declared.
/api/app/database
Some general methods to interact with database.
/api/app/db_config
When the Neo4j container is launched it will initialize a database
with some test data from init.cypher. This initial state does not represent
all data. Model.cypher contains a more complete model.
/api/app/ifcgraph
Script to import data from IFC STEP file to Neo4j.
/api/app/models
Pydantic models used to define the JSON structures of requests and responses.
Models are arranged in files depending on if it is request or response
or both (common) or not at all (other).
/api/app/security
Contains methods for OAuth2 and a method to get the secrets used.
You need to create your own secrets and put them in the /secrets folder.
Secrets are more secure than environment values.
/api/app/templets
Jinja2 templates used to generate some webpages necessary for OAuth2 flow and Documents API flow.
/api/app/requirements.txt
Generated using pipreqs not pip freeze.
## Deployment
To get this code running properly you need:
- your own linux server, on premise or in the cloud
- a domain name and a TLS-certificate for your domain name
- a proxy server, nginx was used
- to set up your proxy server to use HTTPS
- to use proxy as bridge between docker network and remote
- proxy (outside container) and uvicorn (inside container) should access same TLS certificate
- to edit your .env file with values specific for your server and API
- to replace hard coded values in code with specific values for your server and API
- to create three secrets and put them in the /secrets folder
- to put apoc-5.7.0-core and apoc-5.7.0-extended jars in /neo4j/plugins folder
- to put an ifcopenshell.zip in /api/app/ifcopenshell folder
- Docker and Neo4j software
- a favicon at /favicon.ico
- to run "bash restart.sh" to start and restart containers
- a BCF client, registered with client ID and secret
This code was developed and tested using Solibri Office as BCF client, however:
Solibri Office can only connect to pre-registered servers. This means that you cannot
connect to your server using a regular version of Solibri Office. If you want to use Solibri,
then you will have to first kindly ask Solibri if they want to add your server
to the list of registered servers. An alternative is to develop your own BCF client
or to use this existing BCF API client module for python to build your own client script:
https://pypi.org/project/bcf-client/
I do not know of any working BCF API client plugins for Revit or Archicad that allows
connecting to arbitrary BCF API servers. It seems like most plugins instead were developed to
only connect to a specific BCF API server. I think that is contradictory because BCF is an open standard
intended to facilitate communication between multiple clients and servers.
A BCF API client that can connect to multiple arbitrary BCF API servers would be a good thing to develop.
## TODO
The original intention of the code was just to create a prototype for private testing.
Improvements are needed to collaborate on this code or to deploy this system in production.
Suggested improvements:
- General code structure refactoring
- Better commenting
- Testing scripts, unit testing
- Better error handling
- More consistent logging
- Easier deployment
- User registration and admin system
- BCF client registration functionality
- OData to Cypher converter
- Documents API and BCF API integration
- More complete OpenAPI documentation of routes
- More complete documentation of pydantic models
- Improvements on IFC-graph
- BCF event logging
- Implement also the routes that Solibri Office does not support
- Replace hard coded values with .env values
- Improve security before any kind of deployment in production
- Making full use of native IFC (for example allowing BCF API to edit IFC directly using BIM snippets)
- Integrate BCF API BIM snippets editing with GIT commits
## Prototype
This code was developed and tested on a system using:
- Ubuntu 20
- Nginx 1.18
- Lets encrypt, certbot
- Docker 20
## Suggested reading
### API
- FastAPI documentation
https://fastapi.tiangolo.com/
- Building Python Microservices with FastAPI
https://www.packtpub.com/product/building-python-microservices-with-fastapi/9781803245966
- Building Python Web APIs with FastAPI
https://www.packtpub.com/product/building-python-web-apis-with-fastapi/9781801076630
- Building Data Science Applications with FastAPI - Second Edition
https://www.packtpub.com/product/building-data-science-applications-with-fastapi-second-edition/9781837632749
- Pydantic docs
https://docs.pydantic.dev/latest/
- OData v4
https://www.odata.org/documentation/
### OAuth2
- Mastering OAuth 2.0
https://www.packtpub.com/product/mastering-oauth-20/9781784395407
- OAuth 2.0 Cookbook
https://www.packtpub.com/product/oauth-20-cookbook/9781788295963
- OAuth2 with Password (and hashing), Bearer with JWT tokens
https://fastapi.tiangolo.com/ur/tutorial/security/oauth2-jwt/
- Authorization Code Flow
https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow
- What is the OAuth 2.0 Authorization Code Grant Type?
https://developer.okta.com/blog/2018/04/10/oauth-authorization-code-grant-type
### Neo4j and Cypher
- Neo4j Cypher Manual
https://neo4j.com/docs/cypher-manual/current/introduction/
- Using Neo4j from python
https://neo4j.com/docs/getting-started/languages-guides/neo4j-python/
- Building Neo4j Applications with Python
https://graphacademy.neo4j.com/courses/app-python/
- The Property Graph Model
https://neo4j.com/developer/graph-database/#property-graph
### OpenCDE APIs
- BCF API
https://github.com/buildingSMART/BCF-API
- Documents API
https://github.com/buildingSMART/documents-API
- Foundation API
https://github.com/buildingSMART/foundation-API
- Open CDE APIs OAuth2 Example
https://github.com/buildingSMART/foundation-API/blob/v1.0/OAuth2Examples.md
### Server
- Docker documentation
https://docs.docker.com/
- Docker Compose overview
https://docs.docker.com/compose/
- How to use secrets in Docker Compose
https://docs.docker.com/compose/use-secrets/
- NGINX Reverse Proxy documentation
https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
- Certbot documentation
https://eff-certbot.readthedocs.io/en/stable/
### IFC
- Native IFC
https://github.com/brunopostle/ifcmerge/blob/main/docs/whitepaper.rst
- IFC Specifications Database
https://technical.buildingsmart.org/standards/ifc/ifc-schema-specifications/
- IFC-graph for facilitating building information access and query
https://www.sciencedirect.com/science/article/pii/S0926580523000389
- IFCOpenShell documentation
https://blenderbim.org/docs-python/
### Frontend
- Jinja2 Documentation
https://svn.python.org/projects/external/Jinja-2.1.1/docs/_build/html/index.html
- Bootstrap
https://getbootstrap.com/docs/4.1/getting-started/introduction/
- jQuery
https://api.jquery.com/
## License
The GNU Affero General Public License is a free,
copyleft license for software and other kinds of works,
specifically designed to ensure cooperation with the community
in the case of network server software.
https://www.gnu.org/licenses/agpl-3.0.en.html
See the /LICENSE file.
+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>
+99
View File
@@ -0,0 +1,99 @@
# docker-compose up --build --force-recreate --detach
# docker-compose up = Create and start containers.
# --build = Build images before starting containers.
# --force-recreate = Recreate containers even if their configuration and image havent changed.
# --detach = Detached mode: Run containers in the background.
version: '3.7'
networks:
kontroll_network:
driver: bridge
services:
# HTTP: 27474 on server matches 7474 in neo4j container.
# HTTPS: 27473 on server matches 7473 in neo4j container.
# Bolt: 27687 on server matches 7687 in neo4j container.
# Environment variables are read from the .env file.
# The .env file should be placed at the root of the project directory next to your docker-compose.yml file.
# APOC core and extended must be loaded from /neo4j/plugins folder.
neo4j:
container_name: kontroll_neo4j
image: library/neo4j:5.7.0-community
restart: unless-stopped
ports:
- "27474:7474"
- "27473:7473"
- "27687:7687"
networks:
- kontroll_network
volumes:
- ./neo4j/data:/data
- ./neo4j/logs:/logs
- ./neo4j/conf:/conf
- ./neo4j/import:/import
- ./neo4j/plugins:/plugins
environment:
- NEO4J_AUTH=${NEO4J_USER}/${NEO4J_INITIAL_PASSWORD}
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
- NEO4J_dbms_security_auth__minimum__password__length=4
- NEO4J_server_http_enabled
- NEO4J_server_http_listen__address=0.0.0.0:7474
- NEO4J_server_https_enabled
- NEO4J_server_https_listen__address=0.0.0.0:7473
- NEO4J_server_bolt_enabled
- NEO4J_server_bolt_listen__address=0.0.0.0:7687
secrets:
- neo4j_password
# depends_on: Dependency services must be started before starting a dependent service.
# HTTPS: 24443 on server matches 8000 in kontroll_fastapi container.
# ./etc/letsencrypt contains HTTPS keys and certificates.
fastapi:
container_name: kontroll_fastapi
build:
context: ./api
dockerfile: Dockerfile
target: server
depends_on:
- neo4j
restart: unless-stopped
ports:
- "24443:8000"
networks:
- kontroll_network
volumes:
- ./etc/letsencrypt:/etc/letsencrypt
- ./api/app/logs:/code/app/logs
- ./api/app/data:/code/app/data
environment:
- NEO4J_USER=${NEO4J_USER}
- NEO4J_INITIAL_PASSWORD=${NEO4J_INITIAL_PASSWORD}
- NEO4J_URI=${NEO4J_URI}
- KONTROLL_BASE_URL=${KONTROLL_BASE_URL}
- KONTROLL_CLIENT_ID=${KONTROLL_CLIENT_ID}
- KONTROLL_CLIENT_NAME=${KONTROLL_CLIENT_NAME}
- SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS=${SECURITY_AUTHORIZATION_CODE_EXPIRE_SECONDS}
- SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS=${SECURITY_ACCESS_TOKEN_EXPIRE_SECONDS}
- SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS=${SECURITY_REFRESH_TOKEN_EXPIRE_SECONDS}
- SECURITY_ALGORITHM=${SECURITY_ALGORITHM}
- SESSION_URL_VALIDITY_SECONDS=${SESSION_URL_VALIDITY_SECONDS}
- SESSION_MAX_FILE_SIZE_BYTES=${SESSION_MAX_FILE_SIZE_BYTES}
secrets:
- neo4j_password
- kontroll_client_secret
- security_secret_key
secrets:
neo4j_password:
file: ./secrets/neo4j_password.txt
kontroll_client_secret:
file: ./secrets/kontroll_client_secret.txt
security_secret_key:
file: ./secrets/security_secret_key.txt
+8
View File
@@ -0,0 +1,8 @@
apoc.ttl.enabled=true
apoc.ttl.schedule=5
apoc.import.file.enabled=true
apoc.export.file.enabled=true
# you have to include
# - apoc-5.7.0-core.jar
# - apoc-5.7.0-extended.jar
# in /neo4j/plugins folder
Binary file not shown.
@@ -1,444 +0,0 @@
{
"Projects":[{
"project_id": "F445F4F2-4D02-4B2A-B612-5E456BEF9137",
"name": "Example project 1",
"authorization": {
"project_actions": [
"createTopic",
"createDocument"
]
}
},
{
"project_id": "A233FBB2-3A3B-EFF4-C123-DE22ABC8414",
"name": "Example project 2",
"authorization": {
"project_actions": []
}
}],
"Extensions":[{
"topic_type": [
"Information",
"Error"
],
"topic_status": [
"Open",
"Closed",
"ReOpened"
],
"topic_label": [
"Architecture",
"Structural",
"MEP"
],
"snippet_type": [
".ifc",
".csv"
],
"priority": [
"Low",
"Medium",
"High"
],
"users": [
"Architect@example.com",
"BIM-Manager@example.com",
"bob_heater@example.com"
],
"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"
]
}],
"Topics": [{
"guid": "A245F4F2-2C01-B43B-B612-5E456BEF8116",
"server_assigned_id": "ISSUE-00001",
"creation_author": "Architect@example.com",
"title": "Example topic 1",
"labels": [
"Architecture",
"Structural"
],
"creation_date": "2013-10-21T17:34:22.409Z"
}, {
"guid": "A211FCC2-3A3B-EAA4-C321-DE22ABC8414",
"server_assigned_id": "ISSUE-00078",
"creation_author": "Architect@example.com",
"title": "Example topic 2",
"labels": [
"Architecture",
"Heating",
"Electrical"
],
"creation_date": "2014-11-19T14:24:11.316Z"
},
{
"guid": "B345F4F2-3A04-B43B-A713-5E456BEF8228",
"server_assigned_id": "ISSUE-00549",
"creation_author": "Architect@example.com",
"creation_date": "2016-08-01T17:34:22.409Z",
"topic_type": "Clash",
"topic_status": "open",
"title": "Example topic 3",
"priority": "high",
"labels": [
"Architecture",
"Heating"
],
"assigned_to": "harry.muster@example.com",
"bim_snippet": {
"snippet_type": "clash",
"is_external": true,
"reference": "https://example.com/bcf/1.0/ADFE23AA11BCFF444122BB",
"reference_schema": "https://example.com/bcf/1.0/clash.xsd"
},
"authorization": {
"topic_actions": [
"createComment",
"createViewpoint"
]
}
}
],
"Files":[{
"display_information": [{
"field_display_name": "Model Name",
"field_value": "ARCH-Z100-051"
}, {
"field_display_name": "Revision Date",
"field_value": "May 3 2020"
}],
"file": {
"ifc_project": "0J$yPqHBD12v72y4qF6XcD",
"file_name": "OfficeBuilding_Architecture_0001.ifc",
"reference": "https://example.com/files/0J$yPqHBD12v72y4qF6XcD_0001.ifc"
}
}, {
"display_information": [{
"field_display_name": "Model Name",
"field_value": "MEP-Z100-015"
}, {
"field_display_name": "Revision Date",
"field_value": "Apr 30 2020"
}],
"file": {
"ifc_project": "3hwBHP91jBRwPsmyf$3Hea",
"file_name": "OfficeBuilding_Heating_0003.ifc",
"reference": "cf37bae6-0900-46be-b37f-b34754fe0b4a"
}
}],
"Comments":[
{
"guid": "C4215F4D-AC45-A43A-D615-AA456BEF832B",
"date": "2016-08-01T12:34:22.409Z",
"author": "max.muster@example.com",
"comment": "Clash found",
"topic_guid": "B345F4F2-3A04-B43B-A713-5E456BEF8228",
"authorization": {
"comment_actions": [
"update"
]
}
},
{
"guid": "A333FCA8-1A31-CAAC-A321-BB33ABC8414",
"date": "2016-08-01T14:24:11.316Z",
"author": "bob.heater@example.com",
"comment": "will rework the heating model",
"topic_guid": "B345F4F2-3A04-B43B-A713-5E456BEF8228"
}
],
"RelatedTopics":[{
"related_topic_guid": "db49df2b-0e42-473b-a3ee-f7b785d783c4"
}, {
"related_topic_guid": "6963a846-54d1-4050-954d-607cd5e48aa3"
}],
"Viewpoints":[{
"guid": "b24a82e9-f67b-43b8-bda0-4946abf39624",
"perspective_camera": {
"camera_view_point": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"camera_direction": {
"x": 1.0,
"y": 1.0,
"z": 2.0
},
"camera_up_vector": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"field_of_view": 90.0,
"aspect_ratio": 1.33
},
"lines": [{
"start_point": {
"x": 2.0,
"y": 1.0,
"z": 1.0
},
"end_point": {
"x": 0.0,
"y": 1.0,
"z": 0.7
}
}],
"clipping_planes": [{
"location": {
"x": 0.7,
"y": 0.3,
"z": -0.2
},
"direction": {
"x": 1.0,
"y": 0.4,
"z": 0.1
}
}]
}, {
"guid": "a11a82e7-e66c-34b4-ada1-5846abf39133",
"perspective_camera": {
"camera_view_point": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"camera_direction": {
"x": 1.0,
"y": 1.0,
"z": 2.0
},
"camera_up_vector": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"field_of_view": 90.0,
"aspect_ratio": 1.33
},
"lines": [{
"start_point": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"end_point": {
"x": 0.0,
"y": 0.0,
"z": 0.0
}
}],
"clipping_planes": [{
"location": {
"x": 0.5,
"y": 0.5,
"z": 0.5
},
"direction": {
"x": 1.0,
"y": 0.0,
"z": 0.0
}
}],
"coloring": [
{
"color": "#ff0000",
"components": [
{
"ifc_guid": "2MF28NhmDBiRVyFakgdbCT",
"originating_system": "Example CAD Application",
"authoring_tool_id": "EXCAD/v1.0"
},
{
"ifc_guid": "3$cshxZO9AJBebsni$z9Yk"
}
]
}
],
"visibility": {
"default_visibility": true,
"exceptions": [
{
"ifc_guid": "2MF28NhmDBiRVyFakgdbCT",
"originating_system": "Example CAD Application",
"authoring_tool_id": "EXCAD/v1.0"
},
{
"ifc_guid": "3$cshxZO9AJBebsni$z9Yk"
}
],
"view_setup_hints": {
"spaces_visible": true,
"space_boundaries_visible": false,
"openings_visible": true
}
}
},
{
"guid": "a11a82e7-e66c-34b4-ada1-5846abf39133",
"index": 10,
"perspective_camera": {
"camera_view_point": {
"x": 0,
"y": 0,
"z": 0
},
"camera_direction": {
"x": 1,
"y": 1,
"z": 2
},
"camera_up_vector": {
"x": 0,
"y": 0,
"z": 1
},
"field_of_view": 90,
"aspect_ratio": 1.33
},
"lines": [
{
"start_point": {
"x": 1,
"y": 1,
"z": 1
},
"end_point": {
"x": 0,
"y": 0,
"z": 0
}
}
],
"clipping_planes": [
{
"location": {
"x": 0.5,
"y": 0.5,
"z": 0.5
},
"direction": {
"x": 1,
"y": 0,
"z": 0
}
}
],
"bitmaps": [
{
"guid": "20c1cb56-315f-4a0a-922d-ed7a4a8edf55",
"bitmap_type": "jpg",
"location": {
"x": 10,
"y": -10,
"z": 7
},
"normal": {
"x": -1,
"y": 1.25,
"z": 0
},
"up": {
"x": -5.4,
"y": -4.3,
"z": 1
},
"height": 1666
}
],
"snapshot": {
"snapshot_type": "png"
},
"selection": [
{
"ifc_guid": "2MF28NhmDBiRVyFakgdbCT",
"originating_system": "Example CAD Application",
"authoring_tool_id": "EXCAD/v1.0"
},
{
"ifc_guid": "3$cshxZO9AJBebsni$z9Yk"
}
]
}],
"DocumentReferences":[{
"guid": "212ab37a-6122-448e-86fc-86503183b520",
"url": "http://example.com/files/LegalRequirements.pdf",
"description": "The legal requirements for buildings."
}, {
"guid": "6cbfe31d-95c3-4f4d-92a6-420c23698721",
"document_guid": "472ab37a-6122-448e-86fc-86503183b520",
"description": "The building owners global design parameters for buildings."
}],
"Documents":[{
"guid": "472ab37a-6122-448e-86fc-86503183b520",
"filename": "LegalRequirements.pdf"
}, {
"guid": "6cbfe31d-95c3-4f4d-92a6-420c23698721",
"filename": "DesignParameters.pdf"
}],
"Events": [{
"topic_guid": "A211FCC2-3A3B-EAA4-C321-DE22ABC8414",
"date": "2014-11-19T14:24:11.316Z",
"author": "Architect@example.com",
"events": [
{
"type": "status_updated",
"value": "Closed"
}
]
}, {
"topic_guid": "A245F4F2-2C01-B43B-B612-5E456BEF8116",
"date": "2013-10-21T17:34:22.409Z",
"author": "Architect@example.com",
"events": [
{
"type": "type_updated",
"value": "Warning"
}
]
}],
"EventComments":[{
"comment_guid": "C4215F4D-AC45-A43A-D615-AA456BEF832B",
"topic_guid": "A211FCC2-3A3B-EAA4-C321-DE22ABC8414",
"date": "2014-11-19T14:24:11.316Z",
"author": "Architect@example.com",
"events": [
{
"type": "comment_created",
"value": null
}
]
}, {
"comment_guid": "C4215F4D-AC45-A43A-D615-AA456BEF832B",
"topic_guid": "A245F4F2-2C01-B43B-B612-5E456BEF8116",
"date": "2013-10-21T17:34:22.409Z",
"author": "Architect@example.com",
"events": [
{
"type": "viewpoint_updated",
"value": "b24a82e9-f67b-43b8-bda0-4946abf39624"
}
]
}]
}
@@ -1,650 +0,0 @@
# OpenCDE - OpenCDE Python implementation
# Copyright (C) 2021 Prabhat Singh <singh01prabhat@gmail.com>
#
# This file is part of OpenCDE.
#
# OpenCDE is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenCDE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with OpenCDE. If not, see <http://www.gnu.org/licenses/>.
from flask import jsonify, url_for, redirect, render_template, request, session, send_file
from flask_login import login_user, logout_user, login_required, current_user
from flask.blueprints import Blueprint
from foundation.models import User, OAuth2AuthorizationCode, OAuth2Token, OAuth2Client
from run import app, db
import json
import os
from flask_expects_json import expects_json
bcf = Blueprint("bcf", __name__, template_folder="templates", url_prefix="/bcf/3.0")
my_absolute_dirpath = os.path.abspath(os.path.dirname(__file__))
data = open(
f"{my_absolute_dirpath}/project.json",
)
jdata = json.load(data)
schema_path = os.path.join(my_absolute_dirpath, "schemas")
def validate_client(request):
Headers = str.split(request.headers["Authorization"])
token = Headers[1]
access_token = OAuth2Token.query.filter_by(access_token=token).first()
if access_token:
return True
else:
return False
def invalid_user():
response = jsonify({"message": "User not recognized"})
response.status = 401
return response
def invalid_project():
response = jsonify({"message": "Project not found"})
response.status_code = 404
return response
@bcf.route("/projects")
def projects():
if validate_client(request):
return jsonify(jdata["Projects"])
return invalid_user()
@bcf.route("/")
@login_required
def bcf_3():
return "<h1>BCF HOMPAGE</h1>"
@bcf.route("/projects/<project_id>")
def project_details(project_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
return jsonify(project)
return invalid_project()
return invalid_user()
project_put = open(f"{schema_path}/Project/project_PUT.json")
project_put = json.load(project_put)
@bcf.route("/projects/<project_id>", methods=["PUT"])
@expects_json(project_put)
def update_project(project_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
response = jsonify(data)
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/extensions")
def extensions(project_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
return jsonify(jdata["Extensions"])
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics")
def topics(project_id):
if validate_client(request):
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
return jsonify(jdata["Topics"])
return invalid_project()
return invalid_user()
topic_post = open(f"{schema_path}/Collaboration/Topic/topic_POST.json")
topic_post = json.load(topic_post)
@bcf.route("/projects/<project_id>/topics/<topic_id>", methods=["POST"])
@expects_json(topic_post)
def create_topic(project_id, topic_id):
if validate_client(request) and request.method == "POST":
body = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
response = jsonify(body)
response.status_code = 201
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>")
def topic_details(project_id, topic_id):
if validate_client(request):
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(topic)
return invalid_project()
return invalid_user()
topic_put = open(f"{schema_path}/Collaboration/Topic/topic_PUT.json")
topic_put = json.load(topic_put)
@bcf.route("/projects/<project_id>/topics/<topic_id>", methods=["PUT"])
@expects_json(topic_put)
def update_topic(project_id, topic_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(data)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>", methods=["DELETE"])
def delete_topic(project_id, topic_id):
if validate_client(request) and request.method == "DELETE":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify("OK")
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/snippet", methods=["GET"])
def get_snippet(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return send_file(f"{my_absolute_dirpath}/success.txt", download_name="snippet.txt")
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/snippet", methods=["PUT"])
def update_snippet(project_id, topic_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify("PUT Request Successful")
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/files_information", methods=["GET"])
def get_files_information(project_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
return jsonify(jdata["Files"])
return invalid_project()
else:
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/files", methods=["GET"])
def get_files(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify("Get request for files successful")
return invalid_project()
return invalid_user()
file_put = open(f"{schema_path}/Collaboration/File/file_PUT.json")
file_put = json.load(file_put)
@bcf.route("/projects/<project_id>/topics/<topic_id>/files", methods=["PUT"])
@expects_json(file_put)
def update_files(project_id, topic_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(data)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/comments", methods=["GET"])
def get_comments(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(jdata["Comments"])
return invalid_project()
return invalid_user()
comment_post = open(f"{schema_path}/Collaboration/Comment/comment_POST.json")
comment_post = json.load(comment_post)
@bcf.route("/projects/<project_id>/topics/<topic_id>/comments", methods=["POST"])
@expects_json(comment_post)
def create_comments(project_id, topic_id):
if validate_client(request) and request.method == "POST":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
response = jsonify(data)
response.status_code = 201
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/comments/<comment_id>", methods=["GET"])
def get_comment(project_id, topic_id, comment_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for comment in jdata["Comments"]:
if (comment["guid"]) == comment_id:
return jsonify(comment)
return invalid_project()
return invalid_user()
comment_put = open(f"{schema_path}/Collaboration/Comment/comment_PUT.json")
comment_put = json.load(comment_put)
@bcf.route("/projects/<project_id>/topics/<topic_id>/comments/<comment_id>", methods=["PUT"])
@expects_json(comment_put)
def update_comment(project_id, topic_id, comment_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for comment in jdata["Comments"]:
if (comment["guid"]) == comment_id:
return jsonify(data)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/comments/<comment_id>", methods=["DELETE"])
def delete_comment(project_id, topic_id, comment_id):
if validate_client(request) and request.method == "DELETE":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for comment in jdata["Comments"]:
if (comment["guid"]) == comment_id:
return jsonify("OK")
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints", methods=["GET"])
def get_viewpoints(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
return jsonify(jdata["Viewpoints"])
return invalid_project()
return invalid_user()
viewpoint_post = open(f"{schema_path}/Collaboration/Viewpoint/viewpoint_POST.json")
viewpoint_post = json.load(viewpoint_post)
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints", methods=["POST"])
@expects_json(viewpoint_post)
def create_viewpoints(project_id, topic_id):
if validate_client(request) and request.method == "POST":
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
data = request.get_json()
response = jsonify(data)
response.status_code = 201
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>", methods=["GET"])
def get_viewpoint(project_id, topic_id, viewpoint_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint)
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>", methods=["DELETE"])
def delete_viewpoint(project_id, topic_id, viewpoint_id):
if validate_client(request) and request.method == "DELETE":
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint)
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>/snapshot", methods=["GET"])
def get_snapshot(project_id, topic_id, viewpoint_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint["snapshot"])
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>/bitmaps/<bitmap_id>", methods=["GET"])
def get_bitmap(project_id, topic_id, viewpoint_id, bitmap_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
for bitmap in viewpoint["bitmaps"]:
if bitmap["guid"] == bitmap_id:
response = jsonify(bitmap)
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>/selection", methods=["GET"])
def get_selection(project_id, topic_id, viewpoint_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint["selection"])
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>/coloring", methods=["GET"])
def get_coloring(project_id, topic_id, viewpoint_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint["coloring"])
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/viewpoints/<viewpoint_id>/visibility", methods=["GET"])
def get_visibility(project_id, topic_id, viewpoint_id):
if validate_client(request):
for project in jdata["Projects"]:
if project["project_id"] == project_id:
for topic in jdata["Topics"]:
if topic["guid"] == topic_id:
for viewpoint in jdata["Viewpoints"]:
if viewpoint["guid"] == viewpoint_id:
response = jsonify(viewpoint["visibility"])
response.status_code = 200
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/related_topics", methods=["GET"])
def get_related_topics(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(jdata["RelatedTopics"])
return invalid_project()
return invalid_user()
related_topics_put = open(f"{schema_path}/Collaboration/RelatedTopic/related_topic_PUT.json")
related_topics_put = json.load(related_topics_put)
@bcf.route("/projects/<project_id>/topics/<topic_id>/related_topics", methods=["PUT"])
@expects_json(related_topics_put)
def update_related_topics(project_id, topic_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(data)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/document_references", methods=["GET"])
def get_document_references(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(jdata["DocumentReferences"])
return invalid_project()
return invalid_user()
document_reference_post = open(f"{schema_path}/Collaboration/DocumentReference/document_reference_POST.json")
document_reference_post = json.load(document_reference_post)
@bcf.route("/projects/<project_id>/topics/<topic_id>/document_references", methods=["POST"])
@expects_json(document_reference_post)
def create_document_references(project_id, topic_id):
if validate_client(request) and request.method == "POST":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
response = jsonify(data)
response.status_code = 201
return response
return invalid_project()
return invalid_user()
document_reference_put = open(f"{schema_path}/Collaboration/DocumentReference/document_reference_PUT.json")
document_reference_put = json.load(document_reference_put)
@bcf.route(
"/projects/<project_id>/topics/<topic_id>/document_references/<document_reference_id>",
methods=["PUT"],
)
@expects_json(document_reference_put)
def update_document_references(project_id, topic_id, document_reference_id):
if validate_client(request) and request.method == "PUT":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for document in jdata["DocumentReferences"]:
if (document["guid"]) == document_reference_id:
return jsonify(data)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/documents", methods=["GET"])
def get_documents(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
return jsonify(jdata["Documents"])
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/documents", methods=["POST"])
def create_documents(project_id, topic_id):
if validate_client(request) and request.method == "POST":
data = request.get_json()
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
response = jsonify(data)
response.status_code = 201
return response
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/documents/<document_id>", methods=["GET"])
def get_document(project_id, topic_id, document_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for document in jdata["Documents"]:
if (document["guid"]) == document_id:
return send_file(f"{my_absolute_dirpath}/success.txt", download_name="document.txt")
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/events", methods=["GET"])
def get_events(project_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
return jsonify(jdata["Events"])
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/<topic_id>/events", methods=["GET"])
def get_topic_events(project_id, topic_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for event in jdata["Events"]:
if (event["topic_guid"]) == topic_id:
return jsonify(event)
return invalid_project()
return invalid_user()
@bcf.route("/projects/<project_id>/topics/comments/events", methods=["GET"])
def get_comments_events(project_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
return jsonify(jdata["EventComments"])
return invalid_project()
return invalid_user()
@bcf.route(
"/projects/<project_id>/topics/<topic_id>/comments/<comment_id>/events",
methods=["GET"],
)
def get_comment_events(project_id, topic_id, comment_id):
if validate_client(request) and request.method == "GET":
for project in jdata["Projects"]:
if (project["project_id"]) == project_id:
for topic in jdata["Topics"]:
if (topic["guid"]) == topic_id:
for comment in jdata["EventComments"]:
if (comment["comment_guid"]) == comment_id:
return jsonify(comment)
return invalid_project()
return invalid_user()
@@ -1,10 +0,0 @@
{
"title": "comment_actions",
"required": false,
"type": "array",
"items": {
"type": "string",
"enum": ["update",
"delete"]
}
}
@@ -1,11 +0,0 @@
{
"title": "project_actions",
"required": false,
"type": "array",
"items": {
"type": "string",
"enum": ["update",
"createTopic",
"createDocument"]
}
}
@@ -1,16 +0,0 @@
{
"title": "topic_actions",
"required": false,
"type": "array",
"items": {
"type": "string",
"enum": ["update",
"updateBimSnippet",
"updateRelatedTopics",
"updateDocumentReferences",
"updateFiles",
"createComment",
"createViewpoint",
"delete"]
}
}
@@ -1,7 +0,0 @@
{
"required": false,
"type": "array",
"items": {
"enum": ["delete"]
}
}
@@ -1,47 +0,0 @@
{
"title": "comment_GET",
"type": "object",
"properties": {
"guid": {
"required": true,
"type": "string"
},
"date": {
"required": true,
"type": "string"
},
"author": {
"required": true,
"type": "string"
},
"comment": {
"required": true,
"type": "string"
},
"topic_guid": {
"required": true,
"type": "string"
},
"viewpoint_guid": {
"type": ["string",
"null"]
},
"modified_date": {
"type": ["string",
"null"]
},
"modified_author": {
"type": ["string",
"null"]
},
"authorization": {
"type": "object",
"required": false,
"properties": {
"comment_actions": {
"$ref": "../Action/comment_actions.json"
}
}
}
}
}
@@ -1,18 +0,0 @@
{
"title": "comment_POST",
"type": "object",
"properties": {
"guid": {
"type": ["string",
"null"]
},
"comment": {
"required": true,
"type": "string"
},
"viewpoint_guid": {
"type": ["string",
"null"]
}
}
}
@@ -1,14 +0,0 @@
{
"title": "comment_PUT",
"type": "object",
"properties": {
"comment": {
"required": true,
"type": "string"
},
"viewpoint_guid": {
"type": ["string",
"null"]
}
}
}
@@ -1,14 +0,0 @@
{
"title": "document_GET",
"type": "object",
"properties": {
"guid": {
"required": true,
"type": "string"
},
"filename": {
"required": true,
"type": "string"
}
}
}
@@ -1,21 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "document_reference_GET",
"description": "Schema for a single document reference GET, BCF REST API.",
"type": "object",
"properties": {
"guid": {
"required": true,
"type": "string"
},
"document_guid": {
"type": ["string", "null"]
},
"url": {
"type": ["string", "null"]
},
"description": {
"type": ["string", "null"]
}
}
}
@@ -1,18 +0,0 @@
{
"title": "document_reference_POST",
"type": "object",
"properties": {
"guid": {
"type": ["string", "null"]
},
"document_guid": {
"type": ["string", "null"]
},
"url": {
"type": ["string", "null"]
},
"description": {
"type": ["string", "null"]
}
}
}
@@ -1,15 +0,0 @@
{
"title": "document_reference_PUT",
"type": "object",
"properties": {
"document_guid": {
"type": ["string", "null"]
},
"url": {
"type": ["string", "null"]
},
"description": {
"type": ["string", "null"]
}
}
}
@@ -1,29 +0,0 @@
{
"title": "comment_event_GET",
"type": "object",
"properties": {
"comment_guid": {
"required": true,
"type": "string"
},
"topic_guid": {
"required": true,
"type": "string"
},
"date": {
"required": true,
"type": "string"
},
"author": {
"required": true,
"type": "string"
},
"actions": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "event_action.json"
}
}
}
}
@@ -1,14 +0,0 @@
{
"title": "event_action",
"type": ["object", "null"],
"properties": {
"type": {
"required": true,
"type": "string"
},
"value": {
"type": ["string",
"null"]
}
}
}
@@ -1,25 +0,0 @@
{
"title": "topic_event_GET",
"type": "object",
"properties": {
"topic_guid": {
"required": true,
"type": "string"
},
"date": {
"required": true,
"type": "string"
},
"author": {
"required": true,
"type": "string"
},
"actions": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "event_action.json"
}
}
}
}
@@ -1,28 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "file_GET",
"description": "Schema for a single file GET, BCF REST API.",
"type": "object",
"properties": {
"ifc_project": {
"type": ["string",
"null"]
},
"ifc_spatial_structure_element": {
"type": ["string",
"null"]
},
"filename": {
"type": ["string",
"null"]
},
"date": {
"type": ["string",
"null"]
},
"reference": {
"type": ["string",
"null"]
}
}
}
@@ -1,26 +0,0 @@
{
"title": "file_PUT",
"type": "object",
"properties": {
"ifc_project": {
"type": ["string",
"null"]
},
"ifc_spatial_structure_element": {
"type": ["string",
"null"]
},
"filename": {
"type": ["string",
"null"]
},
"date": {
"type": ["string",
"null"]
},
"reference": {
"type": ["string",
"null"]
}
}
}
@@ -1,16 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "project_file_display_information",
"description": "Schema for the display information of a project file, BCF REST API.",
"type": "object",
"properties": {
"field_display_name": {
"required": true,
"type": "string"
},
"field_value": {
"required": true,
"type": "string"
}
}
}
@@ -1,13 +0,0 @@
{
"title": "project_file_information",
"description": "Schema for a single project file information, BCF REST API.",
"type": "object",
"properties": {
"display_information": {
"type": "array",
"items": { "$ref": "project_file_display_information.json" },
"default": []
},
"file": { "$ref": "file_GET.json" }
}
}
@@ -1,8 +0,0 @@
{
"description": "Schema for the listing the response of the GET project files information service",
"title": "project_files_information_GET",
"type": "array",
"items": {
"$ref": "project_file_information.json"
}
}
@@ -1,12 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "related_topic_GET",
"description": "Schema for single related topic GET, BCF REST API.",
"type": "object",
"properties": {
"related_topic_guid": {
"required": true,
"type": "string"
}
}
}
@@ -1,12 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "related_topic_PUT",
"description": "Schema for related topic PUT, BCF REST API.",
"type": "object",
"properties": {
"related_topic_guid": {
"required": true,
"type": "string"
}
}
}
@@ -1,23 +0,0 @@
{
"title": "bim_snippet",
"type": ["object",
"null"],
"properties": {
"snippet_type": {
"required": true,
"type": "string"
},
"is_external": {
"required": true,
"type": "boolean"
},
"reference": {
"required": true,
"type": "string"
},
"reference_schema": {
"required": true,
"type": "string"
}
}
}
@@ -1,101 +0,0 @@
{
"title": "topic_GET",
"type": "object",
"properties": {
"guid": {
"required": true,
"type": "string"
},
"server_assigned_id": {
"required": true,
"type": "string"
},
"topic_type": {
"type": ["string",
"null"]
},
"topic_status": {
"type": ["string",
"null"]
},
"reference_links": {
"type": ["array",
"null"],
"items": {
"type": "string"
}
},
"title": {
"required": true,
"type": "string"
},
"priority": {
"type": ["string",
"null"]
},
"index": {
"type": [
"integer",
"null"
]
},
"labels": {
"type": ["array",
"null"],
"items": {
"type": ["string",
"null"]
}
},
"creation_date": {
"required": true,
"type": "string"
},
"creation_author": {
"required": true,
"type": "string"
},
"modified_date": {
"type": "string"
},
"modified_author": {
"type": ["string",
"null"]
},
"assigned_to": {
"type": ["string",
"null"]
},
"stage": {
"type": ["string",
"null"]
},
"description": {
"type": ["string",
"null"]
},
"bim_snippet": {
"$ref": "bim_snippet.json"
},
"due_date": {
"type": ["string",
"null"]
},
"authorization": {
"type": "object",
"required": false,
"properties": {
"topic_actions": {
"$ref": "../Action/topic_actions.json"
},
"topic_status": {
"type": ["array",
"null"],
"items": {
"type": "string"
}
}
}
}
}
}
@@ -1,68 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "topic_POST",
"description": "Schema for topic POST, BCF REST API.",
"type": "object",
"properties": {
"guid": {
"type": ["string",
"null"]
},
"topic_type": {
"type": ["string",
"null"]
},
"topic_status": {
"type": ["string",
"null"]
},
"reference_links": {
"type": ["array",
"null"],
"items": {
"type": "string"
}
},
"title": {
"required": true,
"type": "string"
},
"priority": {
"type": ["string",
"null"]
},
"index": {
"type": [
"integer",
"null"
]
},
"labels": {
"type": ["array",
"null"],
"items": {
"type": ["string",
"null"]
}
},
"assigned_to": {
"type": ["string",
"null"]
},
"stage": {
"type": ["string",
"null"]
},
"description": {
"type": ["string",
"null"]
},
"bim_snippet": {
"$ref": "bim_snippet.json"
},
"due_date": {
"type": ["string",
"null"]
}
}
}
@@ -1,64 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "topic_PUT",
"description": "Schema for topic PUT, BCF REST API.",
"type": "object",
"properties": {
"topic_type": {
"type": ["string",
"null"]
},
"topic_status": {
"type": ["string",
"null"]
},
"reference_links": {
"type": ["array",
"null"],
"items": {
"type": "string"
}
},
"title": {
"required": true,
"type": "string"
},
"priority": {
"type": ["string",
"null"]
},
"index": {
"type": [
"integer",
"null"
]
},
"labels": {
"type": ["array",
"null"],
"items": {
"type": ["string",
"null"]
}
},
"assigned_to": {
"type": ["string",
"null"]
},
"stage": {
"type": ["string",
"null"]
},
"description": {
"type": ["string",
"null"]
},
"bim_snippet": {
"$ref": "bim_snippet.json"
},
"due_date": {
"type": ["string",
"null"]
}
}
}
@@ -1,24 +0,0 @@
{
"type": "object",
"properties": {
"guid": {
"type": "string"
},
"bitmap_type": {
"type": "string",
"enum": ["jpg", "png"]
},
"location": {
"$ref": "location.json"
},
"normal": {
"$ref": "direction.json"
},
"up": {
"$ref": "direction.json"
},
"height": {
"type": "number"
}
}
}
@@ -1,25 +0,0 @@
{
"type": "object",
"properties": {
"bitmap_type": {
"type": "string",
"enum": ["jpg", "png"]
},
"bitmap_data": {
"type": "string",
"format": "base64"
},
"location": {
"$ref": "location.json"
},
"normal": {
"$ref": "direction.json"
},
"up": {
"$ref": "direction.json"
},
"height": {
"type": "number"
}
}
}
@@ -1,13 +0,0 @@
{
"title": "clipping_plane",
"type": ["object",
"null"],
"properties": {
"location": {
"$ref": "location.json"
},
"direction": {
"$ref": "direction.json"
}
}
}
@@ -1,12 +0,0 @@
{
"title": "coloring",
"type": "object",
"properties": {
"color": {
"type": "string"
},
"components": {
"$ref": "component_list.json"
}
}
}
@@ -1,14 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "coloring_GET",
"description": "Schema for coloring GET, BCF REST API.",
"type": "object",
"properties": {
"coloring": {
"type": ["array", "null"],
"items": {
"$ref": "coloring.json"
}
}
}
}
@@ -1,15 +0,0 @@
{
"title": "component",
"type": ["object"],
"properties": {
"ifc_guid": {
"type": ["string", "null"]
},
"originating_system": {
"type": ["string", "null"]
},
"authoring_tool_id": {
"type": ["string", "null"]
}
}
}
@@ -1,7 +0,0 @@
{
"title": "component list",
"type": ["array", "null"],
"items": {
"$ref": "component.json"
}
}
@@ -1,18 +0,0 @@
{
"title": "components",
"type": ["object", "null"],
"properties": {
"selection": {
"$ref": "component_list.json"
},
"coloring": {
"type": ["array", "null"],
"items": {
"$ref": "coloring.json"
}
},
"visibility": {
"$ref": "visibility.json"
}
}
}
@@ -1,15 +0,0 @@
{
"title": "direction",
"type": "object",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"z": {
"type": "number"
}
}
}
@@ -1,12 +0,0 @@
{
"title": "line",
"type": ["object", "null"],
"properties": {
"start_point": {
"$ref": "point.json"
},
"end_point": {
"$ref": "point.json"
}
}
}
@@ -1,16 +0,0 @@
{
"title": "location",
"type": ["object",
"null"],
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"z": {
"type": "number"
}
}
}
@@ -1,22 +0,0 @@
{
"title": "orthogonal_camera",
"type": ["object",
"null"],
"properties": {
"camera_view_point": {
"$ref": "point.json"
},
"camera_direction": {
"$ref": "direction.json"
},
"camera_up_vector": {
"$ref": "direction.json"
},
"view_to_world_scale": {
"type": "number"
},
"aspect_ratio": {
"type": "number"
}
}
}
@@ -1,22 +0,0 @@
{
"title": "perspective_camera",
"type": ["object",
"null"],
"properties": {
"camera_view_point": {
"$ref": "point.json"
},
"camera_direction": {
"$ref": "direction.json"
},
"camera_up_vector": {
"$ref": "direction.json"
},
"field_of_view": {
"type": "number"
},
"aspect_ratio": {
"type": "number"
}
}
}
@@ -1,15 +0,0 @@
{
"title": "point",
"type": "object",
"properties": {
"x": {
"type": "number"
},
"y": {
"type": "number"
},
"z": {
"type": "number"
}
}
}
@@ -1,11 +0,0 @@
{
"$schema": "http://json-schema.org/draft-03/schema#",
"title": "selection_GET",
"description": "Schema for selection GET, BCF REST API.",
"type": "object",
"properties": {
"selection": {
"$ref": "component_list.json"
}
}
}
@@ -1,10 +0,0 @@
{
"title": "snapshot_GET",
"type": ["object", "null"],
"properties": {
"snapshot_type": {
"type": ["string"],
"enum": ["jpg", "png"]
}
}
}
@@ -1,14 +0,0 @@
{
"title": "snapshot_POST",
"type": ["object", "null"],
"properties": {
"snapshot_type": {
"type": ["string"],
"enum": ["jpg", "png"]
},
"snapshot_data": {
"type": ["string"],
"format": "base64"
}
}
}
@@ -1,18 +0,0 @@
{
"title": "view_setup_hints",
"type": ["object", "null"],
"properties": {
"spaces_visible": {
"type": "boolean",
"default": "false"
},
"space_boundaries_visible": {
"type": "boolean",
"default": "false"
},
"openings_visible": {
"type": "boolean",
"default": "false"
}
}
}
@@ -1,52 +0,0 @@
{
"title": "viewpoint_GET",
"type": "object",
"properties": {
"index": {
"type": [
"integer",
"null"
]
},
"guid": {
"required": true,
"type": "string"
},
"orthogonal_camera": {
"$ref": "orthogonal_camera.json"
},
"perspective_camera": {
"$ref": "perspective_camera.json"
},
"lines": {
"type": ["array", "null"],
"items": {
"$ref": "line.json"
}
},
"clipping_planes": {
"type": ["array", "null"],
"items": {
"$ref": "clipping_plane.json"
}
},
"bitmaps": {
"type": ["array", "null"],
"items": {
"$ref": "bitmap_GET.json"
}
},
"snapshot": {
"$ref": "snapshot_GET.json"
},
"authorization": {
"type": "object",
"required": false,
"properties": {
"viewpoint_actions": {
"$ref": "../Action/viewpoint_actions.json"
}
}
}
}
}
@@ -1,46 +0,0 @@
{
"title": "viewpoint_POST",
"type": [ "object" ],
"properties": {
"guid": {
"type": ["string",
"null"]
},
"index": {
"type": [
"integer",
"null"
]
},
"orthogonal_camera": {
"$ref": "orthogonal_camera.json"
},
"perspective_camera": {
"$ref": "perspective_camera.json"
},
"lines": {
"type": ["array", "null"],
"items": {
"$ref": "line.json"
}
},
"clipping_planes": {
"type": ["array", "null"],
"items": {
"$ref": "clipping_plane.json"
}
},
"bitmaps": {
"type": ["array", "null"],
"items": {
"$ref": "bitmap_POST.json"
}
},
"snapshot": {
"$ref": "snapshot_POST.json"
},
"components": {
"$ref": "components.json"
}
}
}

Some files were not shown because too many files have changed in this diff Show More