diff --git a/src/opencdeserver/.gitignore b/src/opencdeserver/.gitignore new file mode 100644 index 0000000000..8829d1b40f --- /dev/null +++ b/src/opencdeserver/.gitignore @@ -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/ diff --git a/src/opencdeserver/LICENSE b/src/opencdeserver/LICENSE new file mode 100644 index 0000000000..0ad25db4bd --- /dev/null +++ b/src/opencdeserver/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/src/opencdeserver/README.md b/src/opencdeserver/README.md index dc88f5ab4a..2081e0996a 100644 --- a/src/opencdeserver/README.md +++ b/src/opencdeserver/README.md @@ -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. \ No newline at end of file diff --git a/src/opencdeserver/api/Dockerfile b/src/opencdeserver/api/Dockerfile new file mode 100644 index 0000000000..7a277afdeb --- /dev/null +++ b/src/opencdeserver/api/Dockerfile @@ -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. \ No newline at end of file diff --git a/src/opencdeserver/api/__init__.py b/src/opencdeserver/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/api/__init__.py b/src/opencdeserver/api/app/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/api/bcf.py b/src/opencdeserver/api/app/api/bcf.py new file mode 100644 index 0000000000..802bd51b70 --- /dev/null +++ b/src/opencdeserver/api/app/api/bcf.py @@ -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 diff --git a/src/opencdeserver/api/app/api/documents.py b/src/opencdeserver/api/app/api/documents.py new file mode 100644 index 0000000000..9db65bfacd --- /dev/null +++ b/src/opencdeserver/api/app/api/documents.py @@ -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 diff --git a/src/opencdeserver/api/app/api/foundation.py b/src/opencdeserver/api/app/api/foundation.py new file mode 100644 index 0000000000..04a0baa9f4 --- /dev/null +++ b/src/opencdeserver/api/app/api/foundation.py @@ -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= +# &state= +# &scope= +# &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= + + +@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 diff --git a/src/opencdeserver/api/app/api/logging.py b/src/opencdeserver/api/app/api/logging.py new file mode 100644 index 0000000000..2547ac66c2 --- /dev/null +++ b/src/opencdeserver/api/app/api/logging.py @@ -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) diff --git a/src/opencdeserver/api/app/api/user.py b/src/opencdeserver/api/app/api/user.py new file mode 100644 index 0000000000..28d3aa5fbb --- /dev/null +++ b/src/opencdeserver/api/app/api/user.py @@ -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') \ No newline at end of file diff --git a/src/opencdeserver/api/app/data/bitmaps/.empty b/src/opencdeserver/api/app/data/bitmaps/.empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/data/document_parts/.empty b/src/opencdeserver/api/app/data/document_parts/.empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/data/documents/6dbd4d52-14db-11ee-be56-0242ac120002.ifc b/src/opencdeserver/api/app/data/documents/6dbd4d52-14db-11ee-be56-0242ac120002.ifc new file mode 100644 index 0000000000..017230a0a7 --- /dev/null +++ b/src/opencdeserver/api/app/data/documents/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; diff --git a/src/opencdeserver/api/app/data/documents/78860d3c-14db-11ee-be56-0242ac120002.ifc b/src/opencdeserver/api/app/data/documents/78860d3c-14db-11ee-be56-0242ac120002.ifc new file mode 100644 index 0000000000..017230a0a7 --- /dev/null +++ b/src/opencdeserver/api/app/data/documents/78860d3c-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; diff --git a/src/opencdeserver/api/app/data/documents/78860e68-14db-11ee-be56-0242ac120002.ifc b/src/opencdeserver/api/app/data/documents/78860e68-14db-11ee-be56-0242ac120002.ifc new file mode 100644 index 0000000000..017230a0a7 --- /dev/null +++ b/src/opencdeserver/api/app/data/documents/78860e68-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; diff --git a/src/opencdeserver/api/app/data/models/.empty b/src/opencdeserver/api/app/data/models/.empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/data/snapshots/.empty b/src/opencdeserver/api/app/data/snapshots/.empty new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/database/__init__.py b/src/opencdeserver/api/app/database/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/database/neo4j.py b/src/opencdeserver/api/app/database/neo4j.py new file mode 100644 index 0000000000..a3770d23a7 --- /dev/null +++ b/src/opencdeserver/api/app/database/neo4j.py @@ -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() diff --git a/src/opencdeserver/api/app/db_config/arrows.app.json b/src/opencdeserver/api/app/db_config/arrows.app.json new file mode 100644 index 0000000000..d7b3caea72 --- /dev/null +++ b/src/opencdeserver/api/app/db_config/arrows.app.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/src/opencdeserver/api/app/db_config/init.cypher b/src/opencdeserver/api/app/db_config/init.cypher new file mode 100644 index 0000000000..41d18dc4f0 --- /dev/null +++ b/src/opencdeserver/api/app/db_config/init.cypher @@ -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); diff --git a/src/opencdeserver/api/app/db_config/model.cypher b/src/opencdeserver/api/app/db_config/model.cypher new file mode 100644 index 0000000000..64f608ef29 --- /dev/null +++ b/src/opencdeserver/api/app/db_config/model.cypher @@ -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: ""}) \ No newline at end of file diff --git a/src/opencdeserver/api/app/ifcgraph/ifcgraph.py b/src/opencdeserver/api/app/ifcgraph/ifcgraph.py new file mode 100644 index 0000000000..d88359672a --- /dev/null +++ b/src/opencdeserver/api/app/ifcgraph/ifcgraph.py @@ -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 diff --git a/src/opencdeserver/api/app/ifcopenshell/ifcopenshell.zip b/src/opencdeserver/api/app/ifcopenshell/ifcopenshell.zip new file mode 100644 index 0000000000..173d47b9ca Binary files /dev/null and b/src/opencdeserver/api/app/ifcopenshell/ifcopenshell.zip differ diff --git a/src/opencdeserver/api/app/main.py b/src/opencdeserver/api/app/main.py new file mode 100644 index 0000000000..5240019f4b --- /dev/null +++ b/src/opencdeserver/api/app/main.py @@ -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) diff --git a/src/opencdeserver/api/app/models/__init__.py b/src/opencdeserver/api/app/models/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/opencdeserver/api/app/models/bcf_common.py b/src/opencdeserver/api/app/models/bcf_common.py new file mode 100644 index 0000000000..1227b886c9 --- /dev/null +++ b/src/opencdeserver/api/app/models/bcf_common.py @@ -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 diff --git a/src/opencdeserver/api/app/models/bcf_request.py b/src/opencdeserver/api/app/models/bcf_request.py new file mode 100644 index 0000000000..63f225521e --- /dev/null +++ b/src/opencdeserver/api/app/models/bcf_request.py @@ -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 diff --git a/src/opencdeserver/api/app/models/bcf_response.py b/src/opencdeserver/api/app/models/bcf_response.py new file mode 100644 index 0000000000..c4e39eed62 --- /dev/null +++ b/src/opencdeserver/api/app/models/bcf_response.py @@ -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 diff --git a/src/opencdeserver/api/app/models/documents_common.py b/src/opencdeserver/api/app/models/documents_common.py new file mode 100644 index 0000000000..ac67e3f111 --- /dev/null +++ b/src/opencdeserver/api/app/models/documents_common.py @@ -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 diff --git a/src/opencdeserver/api/app/models/documents_not_used.py b/src/opencdeserver/api/app/models/documents_not_used.py new file mode 100644 index 0000000000..f386cb73c3 --- /dev/null +++ b/src/opencdeserver/api/app/models/documents_not_used.py @@ -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] diff --git a/src/opencdeserver/api/app/models/documents_other.py b/src/opencdeserver/api/app/models/documents_other.py new file mode 100644 index 0000000000..bc848dfa6b --- /dev/null +++ b/src/opencdeserver/api/app/models/documents_other.py @@ -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' + } +} diff --git a/src/opencdeserver/api/app/models/documents_request.py b/src/opencdeserver/api/app/models/documents_request.py new file mode 100644 index 0000000000..91ff3c4fb9 --- /dev/null +++ b/src/opencdeserver/api/app/models/documents_request.py @@ -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' + ) diff --git a/src/opencdeserver/api/app/models/documents_response.py b/src/opencdeserver/api/app/models/documents_response.py new file mode 100644 index 0000000000..3b60a47dfe --- /dev/null +++ b/src/opencdeserver/api/app/models/documents_response.py @@ -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] diff --git a/src/opencdeserver/api/app/models/foundation_other.py b/src/opencdeserver/api/app/models/foundation_other.py new file mode 100644 index 0000000000..3762d20272 --- /dev/null +++ b/src/opencdeserver/api/app/models/foundation_other.py @@ -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 diff --git a/src/opencdeserver/api/app/models/other.py b/src/opencdeserver/api/app/models/other.py new file mode 100644 index 0000000000..abd6c5af79 --- /dev/null +++ b/src/opencdeserver/api/app/models/other.py @@ -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 diff --git a/src/opencdeserver/api/app/models/request.py b/src/opencdeserver/api/app/models/request.py new file mode 100644 index 0000000000..e0d34221a5 --- /dev/null +++ b/src/opencdeserver/api/app/models/request.py @@ -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 diff --git a/src/opencdeserver/api/app/models/response.py b/src/opencdeserver/api/app/models/response.py new file mode 100644 index 0000000000..a41e1d9302 --- /dev/null +++ b/src/opencdeserver/api/app/models/response.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + + + diff --git a/src/opencdeserver/api/app/repository/bcf.py b/src/opencdeserver/api/app/repository/bcf.py new file mode 100644 index 0000000000..8c881286ed --- /dev/null +++ b/src/opencdeserver/api/app/repository/bcf.py @@ -0,0 +1,1474 @@ +import base64 +import os +import jsonpickle + +from fastapi import UploadFile, HTTPException + +from uuid import UUID, uuid4 + +from database.neo4j import MyDB, driver + +from models.bcf_request import * +from models.bcf_response import * +from models.request import * + + +class BCFDB(MyDB): + + # implemented + def get_projects(self, current_user: User) -> List[ProjectGET]: + def get_projects_work(tx) -> List[ProjectGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project) + WHERE u.username = $username + AND r1.read = True + RETURN p AS project + """ + results = tx.run(cypher, username=current_user.username) + project_model_list = list() + for result in results: + project_json = self.node_to_json(result.get("project")) + project_model = ProjectGET(**project_json) + project_model_list.append(project_model) + return project_model_list + + with self.driver.session() as session: + return session.execute_read(get_projects_work) + + # implemented + def get_project(self, project_id: UUID, current_user: User) -> ProjectGET: + def get_project_work(tx) -> ProjectGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + RETURN p AS project + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + project_node = first.get("project") + project_dict = self.node_to_json(project_node) + return ProjectGET(**project_dict) + + with self.driver.session() as session: + return session.execute_read(get_project_work) + + # implemented + def put_project(self, project_id: UUID, project: ProjectPUT, current_user: User) -> ProjectGET: + def put_project_work(tx) -> ProjectGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + SET p.name = $project_name + RETURN p AS project + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + project_name=project.name) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + project_node = first.get("project") + project_dict = self.node_to_json(project_node) + return ProjectGET(**project_dict) + + with self.driver.session() as session: + return session.execute_write(put_project_work) + + # implemented + def get_project_extensions(self, project_id: UUID, current_user: User) -> ExtensionsGET: + def get_project_extensions_work(tx) -> ExtensionsGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(e:Extensions) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + RETURN e AS extensions + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + extensions_node = first.get("extensions") + extensions_dict = self.node_to_json(extensions_node) + return ExtensionsGET(**extensions_dict) + + with self.driver.session() as session: + return session.execute_read(get_project_extensions_work) + + # implemented + # returns a collection + def get_topics(self, project_id: str, current_user: User) -> List[TopicGET]: + def get_topics_work(tx) -> List[TopicGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + RETURN t AS topic, ID(t) AS server_assigned_id + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id)) + topic_model_list = list() + for result in results: + topic_json = self.node_to_json(result.get("topic")) + topic_json["server_assigned_id"] = result.get("server_assigned_id") + topic_model = TopicGET(**topic_json) + topic_model_list.append(topic_model) + return topic_model_list + + with self.driver.session() as session: + return session.execute_read(get_topics_work) + + # implemented + def get_topic(self, project_id: UUID, topic_id: UUID, current_user: User) -> TopicGET: + def get_topic_work(tx) -> TopicGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + RETURN t AS topic, ID(t) AS server_assigned_id + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + topic_node = first.get("topic") + topic_dict = self.node_to_json(topic_node) + topic_dict["server_assigned_id"] = first.get("server_assigned_id") + return TopicGET(**topic_dict) + + with self.driver.session() as session: + return session.execute_read(get_topic_work) + + # implemented + def post_topic(self, project_id: UUID, topic: TopicPOST, current_user: User) -> TopicGET: + def post_topic_work(tx) -> UUID: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project) + WHERE u.username = $username + AND r1.createTopic = True + AND p.project_id = $project_id + MERGE (p)-[r2:HAS]->(t:Topic {guid: $guid}) + SET t.creation_date = $creation_date, + t.modified_date = $creation_date, + t.creation_author = $username, + t.modified_author = $username, + t.topic_type = $topic_type, + t.topic_status = $topic_status, + t.title = $title, + t.priority = $priority, + t.index = $index, + t.assigned_to = $assigned_to, + t.stage = $stage, + t.description = $description, + t.due_date = $due_date + RETURN t AS topic, ID(t) AS server_assigned_id + """ + if not hasattr(topic, 'guid') or topic.guid is None: + topic.guid = uuid4() + creation_date = self.timestamp() + print(cypher) + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + guid=str(topic.guid), + creation_date=creation_date, + topic_type=topic.topic_type, + topic_status=topic.topic_status, + title=topic.title, + priority=topic.priority, + index=topic.index, + assigned_to=topic.assigned_to, + stage=topic.stage, + description=topic.description, + due_date=topic.due_date) + summary = result.consume() + if summary.counters.nodes_created < 1: + raise HTTPException(status_code=400, detail="Item not added.") + return topic.guid + + with self.driver.session() as session: + topic_guid = session.execute_write(post_topic_work) + return self.get_topic(project_id, topic_guid, current_user) + + # implemented + def put_topic(self, project_id: UUID, topic_id: UUID, topic: TopicPUT, current_user: User) -> TopicGET: + def put_topic_work(tx) -> bool: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + AND t.guid = $topic_id + SET t.topic_type = $topic_type, + t.topic_status = $topic_status, + t.title = $title, + t.priority = $priority, + t.index = $index, + t.modified_date = $modified_date, + t.modified_author = $username, + t.assigned_to = $assigned_to, + t.stage = $stage, + t.description = $description, + t.due_date = $due_date + RETURN t AS topic, ID(t) AS server_assigned_id + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + topic_type=topic.topic_type, + topic_status=topic.topic_status, + title=topic.title, + priority=topic.priority, + index=topic.index, + modified_date=self.timestamp(), + assigned_to=topic.assigned_to, + stage=topic.stage, + description=topic.description, + due_date=topic.due_date) + summary = result.consume() + if summary.counters.properties_set < 1: + raise HTTPException(status_code=404, detail="Item not found.") + return True + + with self.driver.session() as session: + session.execute_write(put_topic_work) + return self.get_topic(project_id, topic_id, current_user) + + # implemented + + def set_topic_modified_date(self, project_id: UUID, topic_id: UUID, current_user: User) -> bool: + def set_topic_modified_date_work(tx) -> bool: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + AND t.guid = $topic_id + SET t.modified_date = $modified_date + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + modified_date=self.timestamp()) + summary = result.consume() + if summary.counters.properties_set < 1: + raise HTTPException(status_code=404, detail="Item not found.") + return True + + with self.driver.session() as session: + return session.execute_write(set_topic_modified_date_work) + + # implemented + def delete_topic(self, project_id: UUID, topic_id: UUID, current_user: User) -> int: + def delete_topic_work(tx) -> int: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + AND t.guid = $topic_id + DETACH DELETE t + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + result_summary = result.consume() + if result_summary.counters.nodes_deleted < 1: + raise HTTPException(status_code=404, detail="Item not found.") + return result_summary.counters.nodes_deleted + + with self.driver.session() as session: + return session.execute_write(delete_topic_work) + + # implemented + # returns a collection + def get_files_information(self, project_id: UUID, current_user: User) -> List[ProjectFileInformation]: + def get_files_information_work(tx) -> List[ProjectFileInformation]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + RETURN d AS document + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id)) + + project_file_informations = list() + for result in results: + document_json = self.node_to_json(result.get("document")) + display_information_list = list() + display_information_json = { + 'field_display_name': 'File', + 'field_value': document_json['title'] + } + display_information_list.append(display_information_json) + base = os.environ['KONTROLL_BASE_URL'] + "documents/1.0/document/" + base.replace('http://', 'https://') + base.replace('https://', 'open-cde-documents://') + document_id = document_json['document_id'] + version_index = document_json['version_index'] + extra_path = document_id + "/version/" + str(version_index) + reference = base + extra_path + file_get_json = { + 'ifc_project': document_json['ifc_project'], + 'filename': document_json['name'], + 'reference': reference + } + project_file_information = ProjectFileInformation(**{ + 'display_information': display_information_list, + 'file': file_get_json + }) + project_file_informations.append(project_file_information) + return project_file_informations + + # + # BCF API compatible servers offer endpoints to list project model files, which are stored on a CDE. + # To indicate that models provided by BCF API file-references can be downloaded from the CDE using + # the Documents API, the reference part of a file_GET response from the BCF server will include + # the URL scheme (protocol) open-cde-documents and a link to the document_version URL on the CDE. + # + # The actual URL scheme (protocol) that a Client Application should + # use to call the document_version URL should be https. + # + # BCF API project_file_information example: + # + # { + # "display_information": [ + # { + # "field_display_name": "Model Name", + # "field_value": "ARCH-Z100-051" + # } + # ], + # "file": { + # "ifc_project": "0J$yPqHBD12v72y4qF6XcD", + # "file_name": "OfficeBuilding_Architecture_0001.ifc", + # "reference": "open-cde-documents://" + # } + # } + # + # In the example above, the CDE returns a value of + # open-cde-documents:// for the file reference part, + # indicating that a Documents API capable client can call https:// + # to get the document version information. From there on, compatible + # clients can retrieve metadata and the binary content of the file. + # + # class ProjectFileInformation(BaseModel): + # display_information: Optional[List[ProjectFileDisplayInformation]] = None + # file: Optional[FileGET] = 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 + # + + with self.driver.session() as session: + return session.execute_read(get_files_information_work) + + # implemented + # returns a collection + def get_files(self, project_id: UUID, topic_id: UUID, current_user: User) -> List[FileGET]: + def get_files_work(tx) -> List[FileGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:REFERS_TO]->(f:Document:Model) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + RETURN f AS file + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + file_model_list = list() + for result in results: + file_json = self.node_to_json(result.get("file")) + file_model = FileGET(**file_json) + file_model_list.append(file_model) + return file_model_list + + with self.driver.session() as session: + return session.execute_read(get_files_work) + + # implemented + def put_files(self, project_id: UUID, topic_id: UUID, files: List[FilePUT], current_user: User) -> List[FileGET]: + def put_files_work(tx) -> bool: + cypher_delete_references = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:REFERS_TO]->(f:Document:Model) + WHERE u.username = $username + AND r1.updateFiles = True + AND p.project_id = $project_id + AND t.guid = $topic_id + DELETE r3 + """ + tx.run(cypher_delete_references, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + cypher_update_and_attach_references = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(f:Document:Model) + MATCH (u)-[r1]->(p)-[r3:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.updateFiles = True + AND p.project_id = $project_id + AND f.reference = $reference + AND t.guid = $topic_id + SET f.filename = $filename, + f.date = $date, + f.ifc_project = $ifc_project, + f.ifc_spatial_structure_element = $ifc_spatial_structure_element + MERGE (t)-[r4:REFERS_TO]->(f) + RETURN f + """ + for file in files: + result = tx.run(cypher_update_and_attach_references, + username=current_user.username, + project_id=str(project_id), + reference=file.reference, + topic_id=str(topic_id), + filename=file.filename, + date=file.date, + ifc_project=file.ifc_project, + ifc_spatial_structure_element=file.ifc_spatial_structure_element) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + return True + + with self.driver.session() as session: + session.execute_write(put_files_work) + return self.get_files(project_id, topic_id, current_user) + + # implemented + def get_comments(self, project_id: UUID, topic_id: UUID, current_user: User) -> List[CommentGET]: + def get_comments_work(tx) -> List[CommentGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(c:Comment) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + RETURN c AS comment + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + comment_model_list = list() + for result in results: + comment_json = self.node_to_json(result.get("comment")) + comment_json['topic_guid'] = str(topic_id) + comment_model = CommentGET(**comment_json) + comment_model_list.append(comment_model) + return comment_model_list + + with self.driver.session() as session: + return session.execute_read(get_comments_work) + + # implemented + def get_comment(self, project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User) -> CommentGET: + def get_comment_work(tx) -> CommentGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(c:Comment) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND c.guid = $comment_id + RETURN c AS comment + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + comment_node = first.get("comment") + comment_dict = self.node_to_json(comment_node) + comment_dict['topic_guid'] = str(topic_id) + comment = CommentGET(**comment_dict) + return comment + + with self.driver.session() as session: + return session.execute_read(get_comment_work) + # date + # author + # topic_guid + + def post_comment(self, project_id: UUID, topic_id: UUID, comment: CommentPOST, current_user: User) -> CommentGET: + def post_comment_work(tx) -> UUID: + if not hasattr(comment, 'guid') or comment.guid is None: + this_comment_guid = uuid4() + else: + this_comment_guid = UUID(comment.guid) + if comment.viewpoint_guid: + # make sure that given viewpoint_guid exists in database + viewpoints = self.get_viewpoints(project_id, topic_id, current_user) + viewpoint_guids = [viewpoint.guid for viewpoint in viewpoints] + if comment.viewpoint_guid not in viewpoint_guids: + raise HTTPException(status_code=400, detail="Item not added.") + injection_viewpoint = '-[r3:HAS]->(v:Viewpoint)' + injection_viewpoint_selection = 'AND v.guid = $viewpoint_id' + injection_viewpoint_relation = 'MERGE (c2)-[r7:RELATED_TO]->(v)' + else: + injection_viewpoint = '' + injection_viewpoint_selection = '' + injection_viewpoint_relation = '' + if hasattr(comment, 'reply_to_comment_guid'): + # make sure that given reply_to_comment_guid exists in database + get_comments = self.get_comments(project_id, topic_id, current_user) + comment_guids = [get_comment.guid for get_comment in get_comments] + if comment.reply_to_comment_guid not in comment_guids: + raise HTTPException(status_code=400, detail="Item not added.") + injection_existing_comment = 'MATCH (u)-[r1]->(p)-[r2]->(t)-[r4:HAS]->(c1:Comment)' + injection_existing_comment_selection = 'AND c1.guid = $reply_to_comment_id' + injection_existing_comment_relation = 'MERGE (c1)-[r5:THEN]->(c2)' + else: + injection_existing_comment = '' + injection_existing_comment_selection = '' + injection_existing_comment_relation = '' + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)%s + %s + WHERE u.username = $username + AND r1.createComment = True + AND p.project_id = $project_id + AND t.guid = $topic_id + %s + %s + MERGE (t)-[r5:HAS]->(c2:Comment {guid: $comment_id}) + %s + %s + SET c2.date = $creation_date, + c2.modified_date = $creation_date, + c2.author = $username, + c2.modified_author = $username, + c2.comment = $comment + """ % (injection_viewpoint, + injection_existing_comment, + injection_viewpoint_selection, + injection_existing_comment_selection, + injection_viewpoint_relation, + injection_existing_comment_relation) + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(comment.viewpoint_guid), + reply_to_comment_id=str('comment.reply_to_comment_guid'), + comment_id=str(this_comment_guid), + creation_date=self.timestamp(), + comment=comment.comment) + summary = result.consume() + if summary.counters.nodes_created < 1: + raise HTTPException(status_code=400, detail="Item not added.") + return this_comment_guid + + with self.driver.session() as session: + comment_guid = session.execute_write(post_comment_work) + return self.get_comment(project_id, topic_id, comment_guid, current_user) + + def put_comment(self, project_id: UUID, topic_id: UUID, comment_id: UUID, comment: CommentPUT, + current_user: User) -> CommentGET: + def put_comment_work(tx) -> bool: + if comment.viewpoint_guid: + viewpoints = self.get_viewpoints(project_id, topic_id, current_user) + viewpoint_guids = [viewpoint.guid for viewpoint in viewpoints] + if comment.viewpoint_guid not in viewpoint_guids: + raise HTTPException(status_code=400, detail="Item not added.") + injection_new_viewpoint = 'MATCH (t)-[r5:HAS]->(v_new:Viewpoint)' + injection_new_viewpoint_selection = 'WHERE v_new.guid = $new_rtv_id' + injection_new_viewpoint_relation = 'MERGE (c)-[r5:RELATED_TO]->(v)' + else: + injection_new_viewpoint = '' + injection_new_viewpoint_selection = '' + injection_new_viewpoint_relation = '' + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(c:Comment) + WHERE u.username = $username + AND r1.updateComment = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND c.guid = $comment_id + OPTIONAL MATCH (c)-[r4:RELATED_TO]->(v_existing:Viewpoint) + %s + %s + SET c.comment = $comment + SET c.modified_date = $modified_date + SET c.modified_author = $username + DELETE r4 + %s + """ % (injection_new_viewpoint, + injection_new_viewpoint_selection, + injection_new_viewpoint_relation) + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id), + new_rtv_id=str(comment.viewpoint_guid), + comment=comment.comment, + modified_date=self.timestamp()) + summary = result.consume() + if summary.counters.properties_set < 1: + raise HTTPException(status_code=400, detail="Item not added.") + return True + + with self.driver.session() as session: + session.execute_write(put_comment_work) + return self.get_comment(project_id, topic_id, comment_id, current_user) + + # implemented + def delete_comment(self, project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User) -> int: + def delete_comment_work(tx) -> int: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(c:Comment) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND c.guid = $comment_id + DETACH DELETE c + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + comment_id=str(comment_id)) + result_summary = result.consume() + if result_summary.counters.nodes_deleted < 1: + raise HTTPException(status_code=404, detail="Item not found.") + return result_summary.counters.nodes_deleted + + with self.driver.session() as session: + self.set_topic_modified_date(project_id, topic_id, current_user) + return session.execute_write(delete_comment_work) + + @staticmethod + def enum_to_mime(enum_type) -> str: + if str(enum_type) == 'png': + mime_type = 'image/png' + elif str(enum_type) == 'jpg' or str(enum_type) == 'jpeg': + mime_type = 'image/jpeg' + else: + mime_type = 'image/' + str(enum_type) + return mime_type + + # implementing + def post_viewpoint(self, project_id: UUID, topic_id: UUID, viewpoint: ViewpointPOST, + current_user: User) -> ViewpointGET: + def post_viewpoint_work(tx) -> str: + if hasattr(viewpoint, 'snapshot'): + snapshot_type = self.enum_to_mime(viewpoint.snapshot.snapshot_type.value) + snapshot = True + set_snapshot = 'v.snapshot = $snapshot, v.snapshot_type = $snapshot_type,' + else: + snapshot_type = '' + snapshot = False + set_snapshot = '' + cypher_viewpoint = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.createViewpoint = True + AND p.project_id = $project_id + AND t.guid = $topic_id + MERGE (t)-[r3:HAS]->(v:Viewpoint {guid: $guid, index: $index}) + SET v.camera_definition = $camera_definition, + v.camera_view_point = point({x: $cvp_x, y: $cvp_y, z: $cvp_z}), + v.camera_direction = point({x: $cad_x, y: $cad_y, z: $cad_z}), + v.camera_up_vector = point({x: $cuv_x, y: $cuv_y, z: $cuv_z}), + v.view_tws_or_fo_view = $view_tws_or_fo_view, + v.aspect_ratio = $aspect_ratio, + %s + v.default_visibility = $default_visibility, + v.spaces_visible = $spaces_visible, + v.space_boundaries_visible = $space_boundaries_visible, + v.openings_visible = $openings_visible + """ % set_snapshot + if viewpoint.guid is None: + viewpoint.guid = uuid4() + if viewpoint.orthogonal_camera is None: + camera_definition = 'perspective_camera' + view_tws_or_fo_view = viewpoint.perspective_camera.field_of_view + aspect_ratio = viewpoint.perspective_camera.aspect_ratio + cvp = viewpoint.perspective_camera.camera_view_point + cad = viewpoint.perspective_camera.camera_direction + cuv = viewpoint.perspective_camera.camera_up_vector + else: + camera_definition = 'orthogonal_camera' + view_tws_or_fo_view = viewpoint.orthogonal_camera.view_to_world_scale + aspect_ratio = viewpoint.orthogonal_camera.aspect_ratio + cvp = viewpoint.orthogonal_camera.camera_view_point + cad = viewpoint.orthogonal_camera.camera_direction + cuv = viewpoint.orthogonal_camera.camera_up_vector + result = tx.run(cypher_viewpoint, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + guid=str(viewpoint.guid), + index=viewpoint.index, + camera_definition=camera_definition, + cvp_x=cvp.x, cvp_y=cvp.y, cvp_z=cvp.z, + cad_x=cad.x, cad_y=cad.y, cad_z=cad.z, + cuv_x=cuv.x, cuv_y=cuv.y, cuv_z=cuv.z, + view_tws_or_fo_view=view_tws_or_fo_view, + aspect_ratio=aspect_ratio, + snapshot=snapshot, + snapshot_type=snapshot_type, + default_visibility=viewpoint.components.visibility.default_visibility, + spaces_visible=viewpoint.components.visibility.view_setup_hints.spaces_visible, + space_boundaries_visible=viewpoint.components.visibility.view_setup_hints.space_boundaries_visible, + openings_visible=viewpoint.components.visibility.view_setup_hints.openings_visible) + summary = result.consume() + if summary.counters.nodes_created < 1: + print(cypher_viewpoint) + raise HTTPException(status_code=400, detail="Item not added.") + + # LINES + for line in viewpoint.lines: + cypher_line = """ + MATCH (v:Viewpoint) + WHERE v.guid = $guid + MERGE (v)-[r1:HAS]->(l:Line) + SET l.start_point = point({x: $lsp_x, y: $lsp_y, z: $lsp_z}), + l.end_point = point({x: $lep_x, y: $lep_y, z: $lep_z}) + """ + result = tx.run(cypher_line, + guid=str(viewpoint.guid), + lsp_x=line.start_point.x, lsp_y=line.start_point.y, lsp_z=line.start_point.z, + lep_x=line.end_point.x, lep_y=line.end_point.y, lep_z=line.end_point.z) + summary = result.consume() + if summary.counters.nodes_created < 1: + print(cypher_line) + raise HTTPException(status_code=400, detail="Item not added.") + + # CLIPPING PLANES + for clipping_plane in viewpoint.clipping_planes: + cypher_clipping_plane = """ + MATCH (v:Viewpoint) + WHERE v.guid = $guid + MERGE (v)-[r1:HAS]->(cp:ClippingPlane) + SET cp.location = point({x: $cpl_x, y: $cpl_y, z: $cpl_z}), + cp.direction = point({x: $cpd_x, y: $cpd_y, z: $cpd_z}) + """ + result = tx.run(cypher_clipping_plane, + guid=str(viewpoint.guid), + lsp_x=clipping_plane.location.x, lsp_y=clipping_plane.location.y, + lsp_z=clipping_plane.location.z, + lep_x=clipping_plane.direction.x, lep_y=clipping_plane.direction.y, + lep_z=clipping_plane.direction.z) + summary = result.consume() + if summary.counters.nodes_created < 1: + print(cypher_clipping_plane) + raise HTTPException(status_code=400, detail="Item not added.") + + # BITMAPS + for bitmap in viewpoint.bitmaps: + cypher_bitmap = """ + MATCH (v:Viewpoint) + WHERE v.guid = $guid + MERGE (v)-[r1:HAS]->(b:Bitmap) + SET b.type = $type, + b.location = point({x: $bml_x, y: $bml_y, z: $bml_z}), + b.normal = point({x: $bmn_x, y: $bmn_y, z: $bmn_z}), + b.up = point({x: $bmu_x, y: $bmu_y, z: $bmu_z}), + b.height = $height + """ + bitmap_type = self.enum_to_mime(bitmap.bitmap_type) + result = tx.run(cypher_bitmap, + guid=str(viewpoint.guid), + type=bitmap_type, + bml_x=bitmap.location.x, bml_y=bitmap.location.y, bml_z=bitmap.location.z, + bmn_x=bitmap.direction.x, bmn_y=bitmap.direction.y, bmn_z=bitmap.direction.z, + bmu_x=bitmap.direction.x, bmu_y=bitmap.direction.y, bmu_z=bitmap.direction.z, + height=bitmap.height) + summary = result.consume() + if summary.counters.nodes_created < 1: + print(bitmap_type) + raise HTTPException(status_code=400, detail="Item not added.") + + # COMPONENTS + components = {} + for selected_component in viewpoint.components.selection: + if selected_component.ifc_guid not in components: + components[selected_component.ifc_guid] = {} + components[selected_component.ifc_guid]['selected'] = True + components[selected_component.ifc_guid]['originating_system'] = selected_component.originating_system + components[selected_component.ifc_guid]['authoring_tool_id'] = selected_component.authoring_tool_id + for color in viewpoint.components.coloring: + for colored_component in color.components: + if colored_component.ifc_guid not in components: + components[colored_component.ifc_guid] = {} + components[colored_component.ifc_guid]['color'] = color.color + for exception_component in viewpoint.components.visibility.exceptions: + if exception_component.ifc_guid not in components: + components[exception_component.ifc_guid] = {} + components[exception_component.ifc_guid]['visibility_exception'] = True + set_list = list() + + for ifc_guid in components.keys(): + set_list.append('c.ifc_guid = $ifc_guid') + if 'selected' in components[ifc_guid]: + selected = True + originating_system = components[ifc_guid]['originating_system'] + authoring_tool_id = components[ifc_guid]['authoring_tool_id'] + set_list.append('c.selected = $selected') + set_list.append('c.originating_system = $originating_system') + set_list.append('c.authoring_tool_id = $authoring_tool_id') + else: + selected = False + originating_system = False + authoring_tool_id = False + if 'color' in components[ifc_guid]: + color = components[ifc_guid]['color'] + set_list.append('c.color = $color') + else: + color = False + if 'visibility_exception' in components[ifc_guid]: + visibility_exception = True + set_list.append('c.visibility_exception = $visibility_exception') + else: + visibility_exception = False + set_string = ', '.join(set_list) + cypher_component = """ + MATCH (v:Viewpoint) + WHERE v.guid = $viewpoint_id + MERGE (v)-[r1:HAS]->(c:Component {ifc_guid: $ifc_guid}) + SET + """ + cypher_component += set_string + tx.run(cypher_component, + viewpoint_id=str(viewpoint.guid), + ifc_guid=str(ifc_guid), + selected=selected, + originating_system=originating_system, + authoring_tool_id=authoring_tool_id, + color=color, + visibility_exception=visibility_exception) + summary = result.consume() + + # Look for component UUIDs in IFC-graph and relate them + for ifc_guid in components.keys(): + cypher_link = """ + MATCH (prod:IfcProduct) WHERE prod.GlobalId = $uuid + MATCH (c:Component) WHERE c.ifc_guid = $uuid + MERGE (c)-[r:SAME_AS]->(prod) + """ + tx.run(cypher_link, uuid=str(ifc_guid)) + result.consume() + + cypher_link = """ + MATCH (prod:IfcProduct) + -[r1:IsDefinedBy]->(rs:IfcRelationship) + -[r2:RelatingPropertyDefinition]->(ps:IfcPropertySet) + -[r3:HasProperties]->(prop:IfcProperty) + -[r4:NominalValue]->(root:Root) + WHERE prod.GlobalId = $uuid + AND ps.Name = 'Reuse' + AND prop.Name = 'GTIN' + MATCH (c:Component) WHERE c.ifc_guid = $uuid + SET c.GTIN = root.wrappedValue + """ + tx.run(cypher_link, uuid=str(ifc_guid)) + result.consume() + + if summary.counters.nodes_created < 1: + print(cypher_component) + raise HTTPException(status_code=400, detail="Item not added.") + else: + snapshot_path = '' + bitmap_path = '' + try: + snapshot_name = 'snapshot_' + str(viewpoint.guid) + file_ending = '.' + str(viewpoint.snapshot.snapshot_type.value) + snapshot_path = 'data/snapshots/' + snapshot_name + file_ending + with open(snapshot_path, "wb") as snapshot_file: + snapshot_image = base64.b64decode(viewpoint.snapshot.snapshot_data, validate=True) + snapshot_file.write(snapshot_image) + snapshot_file.close() + for bitmap in viewpoint.bitmaps: + bitmap_name = 'bitmap_' + str(bitmap.guid) + file_ending = '.' + str(bitmap.bitmap_type.value) + bitmap_path = 'data/bitmaps/' + bitmap_name + file_ending + with open(bitmap_path, "wb") as bitmap_file: + bitmap_image = base64.b64decode(bitmap.bitmap_data, validate=True) + bitmap_file.write(bitmap_image) + bitmap_file.close() + except Exception as e: + print('could not create file ' + snapshot_path + ' ' + bitmap_path + ' ') + print(e) + raise HTTPException(status_code=400, detail="Item not added.") + return viewpoint.guid + + with self.driver.session() as session: + viewpoint.guid = session.execute_write(post_viewpoint_work) + viewpoint.guid = UUID(viewpoint.guid) + self.set_topic_modified_date(project_id, topic_id, current_user) + return self.get_viewpoint(project_id, topic_id, viewpoint.guid, current_user) + + # implemented + # returns a collection + def get_viewpoints(self, project_id: UUID, topic_id: UUID, current_user: User) -> List[ViewpointGET]: + def get_viewpoints_work(tx) -> List[ViewpointGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + RETURN v.guid AS viewpoint_id + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + viewpoint_model_list = list() + for result in results: + viewpoint_id = result.get("viewpoint_id") + viewpoint_model = self.get_viewpoint(project_id, topic_id, viewpoint_id, current_user) + viewpoint_model_list.append(viewpoint_model) + return viewpoint_model_list + + with self.driver.session() as session: + return session.execute_read(get_viewpoints_work) + + # pending + def get_viewpoint(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User) -> ViewpointGET: + def get_viewpoint_work(tx) -> ViewpointGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + RETURN v AS viewpoint + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + v_node = first.get("viewpoint") + print(v_node) + v = self.node_to_json(v_node) + print(v) + lines = self.get_viewpoint_lines(project_id, topic_id, viewpoint_id, current_user) + clipping_planes = self.get_viewpoint_clipping_planes(project_id, topic_id, viewpoint_id, current_user) + bitmaps = self.get_viewpoint_bitmaps(project_id, topic_id, viewpoint_id, current_user) + snapshot_get = { + 'snapshot_type': v['snapshot_type'].split('/', 2)[1] + } + viewpoint_get = { + 'guid': v['guid'], + 'index': v['index'], + 'lines': lines, + 'clipping_planes': clipping_planes, + 'bitmaps': bitmaps, + 'snapshot': snapshot_get + } + cvp = v['camera_view_point'] + cad = v['camera_direction'] + cuv = v['camera_up_vector'] + camera = { + 'camera_view_point': {'x': cvp.x, 'y': cvp.y, 'z': cvp.z}, + 'camera_direction': {'x': cad.x, 'y': cad.y, 'z': cad.z}, + 'camera_up_vector': {'x': cuv.x, 'y': cuv.y, 'z': cuv.z}, + 'aspect_ratio': v['aspect_ratio'] + } + if v['camera_definition'] == 'orthogonal_camera': + camera['view_to_world_scale'] = v['view_tws_or_fo_view'] + viewpoint_get['orthogonal_camera'] = camera + elif v['camera_definition'] == 'perspective_camera': + camera['field_of_view'] = v['view_tws_or_fo_view'] + viewpoint_get['perspective_camera'] = camera + jsonpickle.dumps(v) + jsonpickle.dumps(viewpoint_get) + return ViewpointGET(**viewpoint_get) + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_work) + + def get_viewpoint_lines(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> List[Line]: + def get_viewpoint_lines_work(tx) -> List[Line]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(l:Line) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + RETURN l AS line + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + line_model_list = list() + for result in results: + line_node = result.get("line") + line_json = { + 'start_point': line_node.start_point, + 'end_point': line_node.end_point + } + line_model = Line(**line_json) + line_model_list.append(line_model) + return line_model_list + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_lines_work) + + def get_viewpoint_clipping_planes(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> List[ClippingPlane]: + def get_viewpoint_clipping_planes_work(tx) -> List[ClippingPlane]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(cp:ClippingPlane) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + RETURN cp AS clipping_plane + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + clipping_plane_model_list = list() + for result in results: + clipping_plane_json = self.node_to_json(result.get("clipping_plane")) + clipping_plane_model = ClippingPlane(**clipping_plane_json) + clipping_plane_model_list.append(clipping_plane_model) + return clipping_plane_model_list + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_clipping_planes_work) + + def get_viewpoint_bitmaps(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> List[BitmapGET]: + def get_viewpoint_bitmaps_work(tx) -> List[BitmapGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(b:Bitmap) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + RETURN b AS bitmap + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + bitmap_model_list = list() + for result in results: + bitmap_json = self.node_to_json(result.get("bitmap")) + bitmap_model = BitmapGET(**bitmap_json) + bitmap_model_list.append(bitmap_model) + return bitmap_model_list + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_bitmaps_work) + + def get_viewpoint_bitmap(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, bitmap_id: UUID, + current_user: User) -> BitmapGET: + def get_viewpoint_bitmap_work(tx) -> BitmapGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(b:Bitmap) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + AND b.guid = $bitmap_id + RETURN b AS bitmap + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id), + bitmap_id=str(bitmap_id)) + bitmap_json = self.node_to_json(result.get("bitmap")) + return BitmapGET(**bitmap_json) + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_bitmap_work) + + def get_viewpoint_snapshot(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User) -> str: + def get_viewpoint_snapshot_work(tx) -> str: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + AND v.snapshot IS NOT NULL + RETURN v.snapshot_type AS snapshot_type + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + snapshot_type = first.get("snapshot_type") + return snapshot_type + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_snapshot_work) + + def get_viewpoint_colored_components(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> ColoringGET: + def get_viewpoint_colored_components_work(tx) -> ColoringGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + AND c.color IS NOT NULL + RETURN c AS component ORDER BY c.color + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + coloring_list = list() + color = '' + coloring = {} + occasion = 0 + for result in results: + component_json = self.node_to_json(result.get("component")) + if component_json['color'] != color: + if occasion > 0: + coloring_list.append(coloring) + occasion += 1 + color = component_json['color'] + coloring = { + 'color': color, + 'components': [] + } + coloring['components'].append(component_json) + coloring_list.append(coloring) + coloring_get = {'coloring': coloring_list} + return ColoringGET(**coloring_get) + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_colored_components_work) + + def get_viewpoint_selected_components(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> SelectionGET: + def get_viewpoint_selected_components_work(tx) -> SelectionGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + AND c.selected IS NOT NULL + RETURN c AS component + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + component_list = list() + for result in results: + component_json = self.node_to_json(result.get("component")) + component_list.append(component_json) + selection_get = {'selection': component_list} + return SelectionGET(**selection_get) + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_selected_components_work) + + def get_viewpoint_components_visibility(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, + current_user: User) -> VisibilityGET: + def get_viewpoint_components_visibility_work(tx) -> VisibilityGET: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + RETURN v AS viewpoint + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + first = result.single() + if first is None: + raise HTTPException(status_code=404, detail="Item not found.") + viewpoint_json = self.node_to_json(first.get("viewpoint")) + view_setup_hints = { + 'spaces_visible': viewpoint_json['spaces_visible'], + 'space_boundaries_visible': viewpoint_json['space_boundaries_visible'], + 'openings_visible': viewpoint_json['openings_visible'] + } + visibility = { + 'default_visibility': viewpoint_json['default_visibility'], + 'view_setup_hints': view_setup_hints + } + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS]->(c:Component) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + AND c.visibility_exception IS NOT NULL + RETURN c AS component + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + component_list = list() + for result in results: + component_json = self.node_to_json(result.get("component")) + component_list.append(component_json) + visibility['exceptions'] = component_list + visibility_get = {'visibility': visibility} + return VisibilityGET(**visibility_get) + + with self.driver.session() as session: + return session.execute_read(get_viewpoint_components_visibility_work) + + # implemented + def delete_viewpoint(self, project_id: UUID, topic_id: UUID, viewpoint_id: UUID, current_user: User) -> int: + def delete_viewpoint_work(tx) -> int: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:HAS]->(v:Viewpoint)-[r4:HAS*]->(n) + WHERE u.username = $username + AND r1.update = True + AND p.project_id = $project_id + AND t.guid = $topic_id + AND v.guid = $viewpoint_id + DETACH DELETE v, n + """ + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + viewpoint_id=str(viewpoint_id)) + result_summary = result.consume() + if result_summary.counters.nodes_deleted < 1: + raise HTTPException(status_code=404, detail="Item not found.") + return result_summary.counters.nodes_deleted + + with self.driver.session() as session: + self.set_topic_modified_date(project_id, topic_id, current_user) + return session.execute_write(delete_viewpoint_work) + + # implemented + # returns a collection + def get_related_topics(self, project_id: UUID, topic_id: UUID, current_user: User) -> List[TopicGET]: + def get_related_topics_work(tx) -> List[TopicGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t1:Topic)-[r3:RELATED_TO]->(t2:Topic) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t1.guid = $topic_id + RETURN t2 AS topic, ID(t2) AS server_assigned_id + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + topic_model_list = list() + for result in results: + topic_json = self.node_to_json(result.get("topic")) + topic_json["server_assigned_id"] = result.get("server_assigned_id") + topic_model = TopicGET(**topic_json) + topic_model_list.append(topic_model) + return topic_model_list + + with self.driver.session() as session: + return session.execute_read(get_related_topics_work) + + def put_related_topics(self, project_id: UUID, topic_id: UUID, related_topics: List[RelatedTopicPUT], + current_user: User) -> List[TopicGET]: + def put_related_topics_work(tx) -> bool: + for related_topic in related_topics: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t1:Topic) + MATCH (p)-[r2:HAS]->(t2:Topic) + WHERE u.username = $username + AND r1.updateRelatedTopics = True + AND p.project_id = $project_id + AND t1.guid = $topic_id + AND t2.guid = $related_topic_id + MERGE (t1)-[r3:RELATED_TO]->(t2:Topic) + """ + tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=topic_id, + related_topic_id=related_topic.related_topic_guid) + return True + + with self.driver.session() as session: + session.execute_write(put_related_topics_work) + self.set_topic_modified_date(project_id, topic_id, current_user) + return self.get_related_topics(project_id, topic_id, current_user) + + # returns a collection + def get_topic_document_references(self, project_id: UUID, topic_id: UUID, + current_user: User) -> List[DocumentReferenceGET]: + def get_topic_document_references_work(tx) -> List[DocumentReferenceGET]: + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic)-[r3:REFERS_TO]->(d:Document) + WHERE u.username = $username + AND r1.read = True + AND p.project_id = $project_id + AND t.guid = $topic_id + RETURN + r3.guid AS reference_guid, + d.guid AS document_guid, + d.url AS document_url, + d.description AS document_description + """ + results = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id)) + document_reference_model_list = list() + for result in results: + document_reference_model = DocumentReferenceGET(**result) + document_reference_model_list.append(document_reference_model) + return document_reference_model_list + + with self.driver.session() as session: + return session.execute_read(get_topic_document_references_work) + + def post_topic_document_reference(self, project_id: UUID, topic_id: UUID, document_reference: DocumentReferencePOST, + current_user: User) -> List[DocumentReferenceGET]: + def post_topic_document_reference_work(tx) -> bool: + if document_reference.guid is None: + document_reference.guid = uuid4() + if document_reference.document_guid is None or len(document_reference.url) > 4 > len( + document_reference.document_guid): + document_reference.document_guid = uuid4() + document_url = 'd.url = $url,' + else: + document_url = '' + document_reference.url = '' + cypher = """ + MATCH (u:User)-[r1:HAS_ACTIONS_ON]->(p:Project)-[r2:HAS]->(t:Topic) + WHERE u.username = $username + AND r1.updateDocumentReferences = True + AND p.project_id = $project_id + AND t.guid = $topic_id + MERGE (t)-[r3:REFERS_TO]->(d:Document {guid: $document_id}) + SET r3.guid: $document_reference_id, + %s + d.description = $description + """ % document_url + result = tx.run(cypher, + username=current_user.username, + project_id=str(project_id), + topic_id=str(topic_id), + document_id=str(document_reference.document_guid), + document_reference_id=str(document_reference.guid), + url=document_reference.url, + description=document_reference.description) + summary = result.consume() + if summary.counters.nodes_created < 1: + raise HTTPException(status_code=400, detail="Item not added.") + return True + + with self.driver.session() as session: + session.execute_write(post_topic_document_reference_work) + self.set_topic_modified_date(project_id, topic_id, current_user) + return self.get_topic_document_references(project_id, topic_id, current_user) + + def put_topic_document_references(self, project_id: UUID, topic_id: UUID, reference_id: UUID, + document_reference: DocumentReferencePUT, + current_user: User) -> List[DocumentReferenceGET]: + document_reference.guid = reference_id + document_reference_post = DocumentReferencePOST(document_reference) + topic_document_references_response = self.post_topic_document_reference(project_id, topic_id, + document_reference_post, current_user) + self.set_topic_modified_date(project_id, topic_id, current_user) + return topic_document_references_response + + # returns a collection + def get_documents(self, project_id: UUID, current_user: User): + return True + + def post_document(self, project_id: UUID, file: UploadFile, current_user: User): + return True + + def get_document(self, project_id: UUID, document_id: UUID, current_user: User): + return True + + # returns a collection + def get_topics_events(self, project_id: UUID, topic_id: UUID, current_user: User): + return True + + # returns a collection + def get_topic_events(self, project_id: UUID, topic_id: UUID, current_user: User): + return True + + # returns a collection + def get_comments_events(self, project_id: UUID, UUID, current_user: User): + return True + + # returns a collection + def get_comment_events(self, project_id: UUID, topic_id: UUID, comment_id: UUID, current_user: User): + return True + + +bcf_db = BCFDB(driver) diff --git a/src/opencdeserver/api/app/repository/documents.py b/src/opencdeserver/api/app/repository/documents.py new file mode 100644 index 0000000000..b48fb886a5 --- /dev/null +++ b/src/opencdeserver/api/app/repository/documents.py @@ -0,0 +1,1000 @@ +import os +import math + +from uuid import UUID, uuid4 +from typing import Union + +from fastapi import HTTPException + +from database.neo4j import MyDB, driver + +from models.documents_request import * +from models.documents_response import * +from models.documents_common import * +from models.documents_other import * + +import ifcopenshell +from py2neo import Graph + +from ifcgraph.ifcgraph import create_full_graph + + +class DOCDB(MyDB): + + # ---- GENERAL FUNCTIONS ---- + + def document_node_to_model(self, document_node): + document_json = self.node_to_json(document_node) + document_json['creation_date'] = self.bcf_time(document_json['creation_date']) + document_json['file_description'] = { + 'name': document_json.pop('name', ''), + 'size_in_bytes': document_json.pop('size_in_bytes', 0) + } + document_json['links'] = self.document_version_links(document_json) + return DocumentVersion(**document_json) + + def get_document(self, document_id, version_index=False): + def get_document_work(tx) -> Union[Document, bool]: + + if version_index is None or version_index is False or not isinstance(version_index, int): + version_index_criteria = '' + else: + version_index_criteria = 'AND d.version_index = $version_index' + + cypher = """ + MATCH (d:Document) + WHERE d.document_id = $document_id + %s + RETURN d AS document + ORDER by d.version_index DESC + LIMIT 1 + """ % version_index_criteria + + result = tx.run(cypher, + document_id=document_id, + version_index=version_index) + + first = result.single() + if first is None: + print('There were no such document version.') + return False + + document_node = first.get('document') + return self.document_node_to_model(document_node) + + with self.driver.session() as session: + return session.execute_read(get_document_work) + + # ---- CUSTOM (NON DOCUMENTS API) UPLOAD FUNCTION ---- + + def create_node_for_uploaded_file(self, selection_session, project, document_version) -> Union[Document, bool]: + def create_document_node_work(tx) -> bool: + cypher = """ + MATCH (ss:Session:Select)<-[r1:HAS]-(u:User)-[r2:HAS_ACTIONS_ON]->(p:Project) + WHERE ss.selection_session = $selection_session + AND p.project_id = $project + CREATE (p)-[r3:CONTAINS]->(d:Document) + SET + d.document_id = $document_id, + d.session_file_id = $session_file_id, + d.version_index = $version_index, + d.version_number = $version_number, + d.creation_date = $creation_date, + d.title = $title, + d.original_file_name = $original_file_name, + d.file_ending = $file_ending, + d.mime_type = $mime_type, + d.file_type = $file_type, + d.name = $name, + d.size_in_bytes = $size_in_bytes + """ + result = tx.run(cypher, + selection_session=selection_session, + project=project, + document_id=document_version.document_id, + session_file_id=document_version.session_file_id, + version_index=document_version.version_index, + version_number=document_version.version_number, + creation_date=document_version.creation_date, + title=document_version.title, + original_file_name=document_version.original_file_name, + file_ending=document_version.file_ending, + mime_type=document_version.mime_type, + file_type=document_version.file_type, + name=document_version.file_description.name, + size_in_bytes=document_version.file_description.size_in_bytes) + + summary = result.consume() + if summary.counters.nodes_created < 1: + raise HTTPException(status_code=400, detail="Document node was not created.") + return True + + def find_document_node_work(tx) -> Union[Document, bool]: + cypher = """ + MATCH (d:Document) + WHERE d.document_id = $document_id + RETURN d AS document + ORDER by d.version_index DESC + LIMIT 1 + """ + + result = tx.run(cypher, document_id=document_version.document_id) + + first = result.single() + if first is None: + print('There were no such document version.') + return False + + document_node = first.get('document') + return self.document_node_to_model(document_node) + + with self.driver.session() as session: + if session.execute_write(create_document_node_work): + return self.get_document(document_version.document_id, document_version.version_index) + + def create_ifc_graph_for_document(self, document_id): + document = self.get_document(document_id) + my_ifc_file = ifcopenshell.open('./data/documents/' + document.file_description.name) + my_graph = Graph(os.environ['NEO4J_URI'], auth=(os.environ['NEO4J_USER'], os.environ['NEO4J_INITIAL_PASSWORD'])) + create_full_graph(my_graph, my_ifc_file) + + # ---- UPLOAD FUNCTIONS ---- + + def post_upload_documents(self, upload_documents: UploadDocuments, + current_user: User) -> DocumentUploadSessionInitialization: + def post_upload_documents_work(tx) -> DocumentUploadSessionInitialization: + + session_uuid = str(uuid4()) + session_callback_timedelta = int(upload_documents.callback.expires_in) + session_url_validity_timedelta = 10 + int(os.environ['SESSION_URL_VALIDITY_SECONDS']) + if not hasattr(upload_documents, 'server_context') or not upload_documents.server_context: + upload_documents.server_context = False + + cypher = """ + MATCH (u:User) + WHERE u.username = $username + CREATE (u)-[r1:HAS]->(s:Session:Upload)-[r2:HAS]->(l:Link) + WITH * + CALL apoc.ttl.expireIn(s, $session_callback_timedelta, 's') + CALL apoc.ttl.expireIn(l, $session_url_timedelta, 's') + SET + s.server_context = $server_context, + s.upload_session = $upload_session, + s.callback = $callback, + s.session_url_timedelta = $session_url_timedelta, + s.session_callback_timedelta = $session_callback_timedelta + """ + + result = tx.run(cypher, + username=current_user.username, + session_callback_timedelta=session_callback_timedelta, + session_url_timedelta=session_url_validity_timedelta, + server_context=upload_documents.server_context, + upload_session=session_uuid, + callback=upload_documents.callback.url) + + summary = result.consume() + if summary.counters.nodes_created < 2: + raise HTTPException(status_code=400, detail="Session or link node was not created.") + + for file in upload_documents.files: + + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:HAS]->(l:Link) + WHERE u.username = $username + AND us.upload_session = $upload_session + CREATE (us)-[r3:CONTAINS]->(d:Document:Temporary) + WITH * + CALL apoc.ttl.expireIn(d, $session_callback_timedelta, 's') + SET + d.original_file_name = $original_file_name, + d.name = $name, + d.session_file_id = $session_file_id, + d.document_id = $document_id, + d.creation_date = $creation_date, + d.file_ending = $file_ending, + d.mime_type = $mime_type, + d.file_type = $file_type + """ + + if not hasattr(file, 'document_id') or not file.document_id: + # When document_id is present, this indicates that + # this upload is a new version of an existing document. + # When not present, we create a new uuid as new document_id. + file.document_id = doc_db.new_uuid() + + if file.file_name.lower().endswith(tuple(file_types)): + file_ending = file.file_name.split('.')[-1].lower() + name = file.document_id + '.' + file_ending + else: + file_ending = '' + name = file.document_id + + print('File ending: ', file_ending) + + mime_type = '' + file_type = '' + + if hasattr(file_types, file_ending): + + print('We have this file ending in dict: ', file_ending) + + mime_type = file_types[file_ending]['mime_type'] + file_type = file_types[file_ending]['file_type'] + + result = tx.run(cypher, + username=current_user.username, + upload_session=session_uuid, + session_callback_timedelta=session_callback_timedelta, + original_file_name=file.file_name, + name=name, + session_file_id=file.session_file_id, + document_id=file.document_id, + creation_date=doc_db.timestamp(), + file_ending=file_ending, + mime_type=mime_type, + file_type=file_type) + + summary = result.consume() + if summary.counters.nodes_created < 1: + raise HTTPException(status_code=400, detail="Document node was not created.") + + session_init = DocumentUploadSessionInitialization(**{ + 'upload_ui_url': os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' \ + + "document-upload?upload_session=" + session_uuid, + 'expires_in': os.environ['SESSION_URL_VALIDITY_SECONDS'], + 'max_size_in_bytes': os.environ['SESSION_MAX_FILE_SIZE_BYTES'], + }) + + return session_init + + with self.driver.session() as session: + return session.execute_write(post_upload_documents_work) + + # implemented + def get_data_for_upload_documents(self, upload_session: UUID) -> UploadDocuments: + def get_data_for_upload_documents_work(tx) -> UploadDocuments: + # to get the documents + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:HAS]->(l:Link), + (us)-[r3:CONTAINS]->(d:Document:Temporary) + WHERE us.upload_session = $upload_session + RETURN d AS document + """ + results = tx.run(cypher, + upload_session=str(upload_session)) + document_list = list() + for result in results: + document_json = self.node_to_json(result.get('document')) + file_to_upload = { + 'file_name': document_json['original_file_name'], + 'session_file_id': document_json['session_file_id'], + 'document_id': document_json['document_id'] + } + document_model = FileToUpload(**file_to_upload) + document_list.append(document_model) + + # to get the documents + cypher = """ + MATCH (p:Project)<-[r1:HAS_ACTIONS_ON]-(u:User)-[r2:HAS]->(us:Session:Upload) + WHERE us.upload_session = $upload_session + RETURN p AS project + """ + results = tx.run(cypher, + upload_session=str(upload_session)) + project_list = list() + for result in results: + project_json = self.node_to_json(result.get('project')) + project = { + 'project_id': project_json['project_id'], + 'name': project_json['name'], + } + print('Project: ', project) + project_list.append(project) + + # to get the user and some session data + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:HAS]->(l:Link) + WHERE us.upload_session = $upload_session + RETURN + us.server_context AS server_context, + us.callback AS callback, + us.session_callback_timedelta AS session_callback_timedelta, + u AS user + """ + result = tx.run(cypher, + upload_session=str(upload_session)) + + first = result.single() + if first is None: + raise HTTPException(status_code=401, detail="No session or link.") + + user_node = first.get('user') + user_dict = self.node_to_json(user_node) + + for_upload_documents_dict = dict() + for_upload_documents_dict['server_context'] = first.get('server_context') + + callback_link = dict() + callback_link['url'] = first.get('callback') + callback_link['expires_in'] = first.get('session_callback_timedelta') + + for_upload_documents_dict['callback'] = callback_link + for_upload_documents_dict['documents'] = document_list + print('Project list: ', project_list) + + for_upload_documents_dict['projects'] = project_list + for_upload_documents_dict['current_user'] = user_dict + + print('Documents list:', document_list) + + for_upload_documents_model = DataForUploadDocuments(**for_upload_documents_dict) + + return for_upload_documents_model + + with self.driver.session() as session: + return session.execute_read(get_data_for_upload_documents_work) + + def save_metadata_for_documents(self, document, upload_session, username): + def save_metadata_for_documents_work(tx) -> DocumentVersion: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND d.session_file_id = $session_file_id + SET + d.version_number = $version_number, + d.version_index = $version_index, + d.title = $title, + d.project = $project + """ + result = tx.run(cypher, + username=username, + upload_session=upload_session, + session_file_id=document.session_file_id, + version_number=document.version_number, + version_index=False, + title=document.title, + project=document.project) + + summary = result.consume() + if summary.counters.properties_set < 4: + raise HTTPException(status_code=400, detail="All properties were not set.") + + return document.session_file_id + + with self.driver.session() as session: + return session.execute_write(save_metadata_for_documents_work) + + def get_upload_instructions(self, upload_session, server_context, document: UploadFileDetail, user: User): + def update_file_size_work(tx) -> str: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND d.session_file_id = $session_file_id + SET + d.size_in_bytes = $size_in_bytes + """ + result = tx.run(cypher, + username=user.username, + upload_session=upload_session, + session_file_id=document.session_file_id, + size_in_bytes=document.size_in_bytes) + summary = result.consume() + if summary.counters.properties_set < 1: + raise HTTPException(status_code=400, detail="Property was not set.") + return document.session_file_id + + with self.driver.session() as session: + session.execute_write(update_file_size_work) + + # link creation + base_url = os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' + upload_session_url = '?upload_session=' + upload_session + upload_complete_url = base_url + 'upload-completion' + upload_session_url + upload_cancellation_url = base_url + 'upload-cancellation' + upload_session_url + upload_completion = LinkData(**{'url': upload_complete_url}) + upload_cancellation = LinkData(**{'url': upload_cancellation_url}) + document_to_upload_model = DocumentToUpload(**{ + 'session_file_id': document.session_file_id, + 'upload_file_parts': list(), + 'upload_completion': upload_completion, + 'upload_cancellation': upload_cancellation + }) + + def add_part_work(tx) -> UUID: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND d.session_file_id = $session_file_id + CREATE (d)-[r3:HAS]->(p:Part) + SET p.number = $part_number, + p.uuid = $part_uuid, + p.content_range_start = $content_range_start, + p.content_range_end = $content_range_end, + p.content_length = $content_length + """ + result = tx.run(cypher, + username=user.username, + upload_session=upload_session, + session_file_id=document.session_file_id, + part_number=part_number, + part_uuid=str(upload_part_uuid), + content_range_start=content_range_start, + content_range_end=content_range_end, + content_length=content_length) + + summary = result.consume() + if summary.counters.nodes_created != 1: + raise HTTPException(status_code=400, detail="Part was not created.") + return upload_part_uuid + + # calculate the number of file parts to send + number_of_parts = math.ceil(int(document.size_in_bytes) / int(os.environ['SESSION_MAX_FILE_SIZE_BYTES'])) + part_length = math.ceil(int(document.size_in_bytes) / number_of_parts) + for part_number in range(number_of_parts): + content_range_start = part_number * part_length + content_range_end = ((part_number + 1) * part_length) - 1 + if content_range_end > document.size_in_bytes: + content_range_end = document.size_in_bytes - 1 + content_length = content_range_end - content_range_start + 1 + upload_part_uuid = uuid4() + upload_part_url = 'upload-part/' + str(upload_part_uuid) + additional_headers = { + 'values': [ + { + 'name': 'Content-Length', + 'value': content_length + } + ] + } + part_instruction = UploadFilePartInstruction(**{ + 'url': base_url + upload_part_url, + 'http_method': 'POST', + 'additional_headers': additional_headers, + 'include_authorization': True, + 'content_range_start': content_range_start, + 'content_range_end': content_range_end + }) + + with self.driver.session() as session: + added_part = session.execute_write(add_part_work) + + if added_part == upload_part_uuid: + document_to_upload_model.upload_file_parts.append(part_instruction) + + return document_to_upload_model + + def user_has_part(self, part_id: str, current_user: User) -> Union[Document, bool]: + def user_has_part_work(tx) -> Union[Document, bool]: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(p:Part) + WHERE u.username = $username + AND p.uuid = $part_id + RETURN d AS document + """ + + result = tx.run(cypher, + username=current_user.username, + part_id=part_id) + + first = result.single() + if first is None: + print('There were no parts in graph!') + return False + + document_node = first.get('document') + document = self.document_node_to_model(document_node) + + print('User had this part ' + part_id + ' in document id: ' + document.document_id) + return document + + with self.driver.session() as session: + return session.execute_read(user_has_part_work) + + def mark_part_as_uploaded(self, part_id: str, current_user: User) -> bool: + def mark_part_as_uploaded_work(tx) -> bool: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(p:Part) + WHERE u.username = $username + AND p.uuid = $part_id + SET p.uploaded = True + """ + + result = tx.run(cypher, + username=current_user.username, + part_id=part_id) + + summary = result.consume() + if summary.counters.properties_set < 1: + raise HTTPException(status_code=400, detail="Part not marked as uploaded.") + else: + return True + + with self.driver.session() as session: + return session.execute_write(mark_part_as_uploaded_work) + + def all_parts_uploaded(self, upload_session: str, current_user: User) -> bool: + def check_uploaded_parts_work(tx) -> bool: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(p:Part) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND p.uploaded IS NULL + RETURN count(p) as parts + """ + + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + first = result.single() + if first is None: + return False + + number_of_parts = first.get('parts') + + print('There are number of parts not uploaded: ', number_of_parts) + + if number_of_parts > 0: + raise HTTPException(status_code=400, detail="All parts not uploaded.") + else: + return True + with self.driver.session() as session: + return session.execute_read(check_uploaded_parts_work) + + + def retrieve_uploaded_parts(self, upload_session: str, current_user: User) -> list: + def retrieve_uploaded_parts_work(tx) -> list: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(p:Part) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND p.uploaded IS NOT NULL + RETURN p as part + ORDER BY part.number + """ + results = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + parts_list = list() + for result in results: + part_json = self.node_to_json(result.get("part")) + parts_list.append(part_json['uuid']) + return parts_list + + with self.driver.session() as session: + return session.execute_read(retrieve_uploaded_parts_work) + + def get_document_from_session(self, upload_session: UUID, current_user: User) -> Document: + def get_document_work(tx) -> Document: + + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND us.upload_session = $upload_session + RETURN d AS document + """ + + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + first = result.single() + document_node = first.get('document') + return self.document_node_to_model(document_node) + + with self.driver.session() as session: + return session.execute_read(get_document_work) + + def document_upload_finish(self, upload_session: UUID, current_user: User, project) -> bool: + def upload_finish_work(tx): + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(part:Part) + WHERE u.username = $username + AND us.upload_session = $upload_session + AND part.uploaded = True + MATCH (proj:Project) + WHERE + proj.project_id = $project + DETACH DELETE part + DELETE r2 + CREATE (proj)-[r4:CONTAINS]->(d) + """ + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session, + project=project) + + summary = result.consume() + if summary.counters.nodes_deleted < 1: + raise HTTPException(status_code=400, detail="Graph could not be upload finished.") + else: + return True + + with self.driver.session() as session: + return session.execute_write(upload_finish_work) + + def get_upload_cancellation(self, upload_session: UUID, current_user: User) -> Union[Document, bool]: + + def retrieve_document_work(tx) -> Union[Document, bool]: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document) + WHERE u.username = $username + AND us.upload_session = $upload_session + RETURN d as document + """ + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + first = result.single() + if first is None: + return False + + document_node = first.get('document') + return self.document_node_to_model(document_node) + + def upload_cancellation_work(tx) -> bool: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Session:Upload)-[r2:CONTAINS]->(d:Document)-[r3:HAS]->(p:Part) + WHERE u.username = $username + AND us.upload_session = $upload_session + DETACH DELETE us, d, p + """ + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + summary = result.consume() + if summary.counters.nodes_deleted < 1: + raise HTTPException(status_code=400, detail="Any nodes could not be deleted.") + else: + return True + + with self.driver.session() as session: + document = session.execute_read(retrieve_document_work) + session.execute_write(upload_cancellation_work) + return document + + # ---- DOWNLOAD FUNCTIONS ---- + + def post_select_documents(self, select_documents: SelectDocuments, + current_user: User) -> DocumentDiscoverySessionInitialization: + def post_select_documents_work(tx) -> DocumentDiscoverySessionInitialization: + + session_uuid = str(uuid4()) + session_callback_timedelta = int(select_documents.callback.expires_in) + session_url_validity_timedelta = 10 + int(os.environ['SESSION_URL_VALIDITY_SECONDS']) + if not hasattr(select_documents, 'server_context') or not select_documents.server_context: + select_documents.server_context = str(uuid4()) + + cypher = """ + MATCH (u:User) + WHERE u.username = $username + CREATE (u)-[r1:HAS]->(s:Session:Select)-[r2:HAS]->(l:Link) + WITH * + CALL apoc.ttl.expireIn(s, $session_callback_timedelta, 's') + CALL apoc.ttl.expireIn(l, $session_url_timedelta, 's') + SET + s.server_context = $server_context, + s.selection_session = $selection_session, + s.callback = $callback, + s.session_url_timedelta = $session_url_timedelta, + s.session_callback_timedelta = $session_callback_timedelta + """ + + result = tx.run(cypher, + username=current_user.username, + session_callback_timedelta=session_callback_timedelta, + session_url_timedelta=session_url_validity_timedelta, + server_context=select_documents.server_context, + selection_session=str(session_uuid), + callback=select_documents.callback.url) + + summary = result.consume() + if summary.counters.nodes_created < 2: + raise HTTPException(status_code=400, detail="Session or link node was not created.") + + session_init_dict = dict() + session_init_dict['select_documents_url'] = os.environ['KONTROLL_BASE_URL'] + 'documents/1.0/' \ + + "document-selection?selection_session=" + session_uuid + session_init_dict['expires_in'] = os.environ['SESSION_URL_VALIDITY_SECONDS'] + session_init_model = DocumentDiscoverySessionInitialization(**session_init_dict) + + return session_init_model + + with self.driver.session() as session: + return session.execute_write(post_select_documents_work) + + def get_data_for_document_selection(self, selection_session: UUID) -> DataForDocumentSelection: + def get_data_for_document_selection_work(tx) -> DataForDocumentSelection: + # to get the projects + cypher = """ + MATCH (l:Link)<-[r1:HAS]-(ss:Session:Select)<-[r2:HAS]-(u:User)-[r3:HAS_ACTIONS_ON]->(p:Project) + WHERE ss.selection_session = $selection_session + RETURN p AS project + ORDER BY project.name + """ + project_results = tx.run(cypher, + selection_session=str(selection_session)) + + project_list = list() + for project_result in project_results: + project_json = self.node_to_json(project_result.get('project')) + project_json['documents'] = list() + project_model = Project(**project_json) + + # to get the documents + cypher = """ + MATCH (l:Link)<-[r1:HAS]-(ss:Session:Select)<-[r2:HAS]-(u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) + WHERE ss.selection_session = $selection_session + AND p.project_id = $project_id + RETURN d AS document + ORDER BY d.title, d.version_index + """ + document_results = tx.run(cypher, + selection_session=str(selection_session), + project_id=str(project_model.project_id)) + + for document_result in document_results: + document_node = document_result.get('document') + document_model = self.document_node_to_model(document_node) + project_model.documents.append(document_model) + + project_list.append(project_model) + + # to get the user and some session data + cypher = """ + MATCH (u:User)-[r1:HAS]->(ss:Session:Select)-[r2:HAS]->(l:Link) + WHERE ss.selection_session = $selection_session + RETURN + ss.server_context AS server_context, + ss.callback AS callback, + ss.session_callback_timedelta AS session_callback_timedelta, + u AS user + """ + + result = tx.run(cypher, + selection_session=str(selection_session)) + + first = result.single() + if first is None: + raise HTTPException(status_code=401, detail="No session or link.") + + user_node = first.get("user") + user_dict = self.node_to_json(user_node) + + for_document_selection_dict = dict() + for_document_selection_dict['server_context'] = first.get('server_context') + + callback_link = dict() + callback_link['url'] = first.get('callback') + callback_link['expires_in'] = first.get('session_callback_timedelta') + + for_document_selection_dict['callback'] = callback_link + for_document_selection_dict['projects'] = project_list + for_document_selection_dict['current_user'] = user_dict + + for_document_selection_model = DataForDocumentSelection(**for_document_selection_dict) + + print('Data for document selection: ', for_document_selection_dict) + + return for_document_selection_model + + with self.driver.session() as session: + return session.execute_read(get_data_for_document_selection_work) + + def post_mark_documents_as_selected(self, all_documents: list, selection_session: UUID) -> DocumentsMarkedAsSelected: + + def mark_documents_as_selected_work(tx) -> DocumentsMarkedAsSelected: + cypher = """ + MATCH (ss:Session:Select)<-[r2:HAS]-(u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) + WHERE ss.selection_session = $selection_session + AND d.document_id = $document_id + MERGE (ss)-[r5:SELECTED]->(d) + """ + + selected_documents_model = DocumentsMarkedAsSelected(**{'documents': list()}) + + for document in all_documents: + result = tx.run(cypher, + selection_session=str(selection_session), + document_id=str(document)) + + summary = result.consume() + + if summary.counters.relationships_created > 0: + selected_documents_model.documents.append(document) + + return selected_documents_model + + with self.driver.session() as session: + return session.execute_write(mark_documents_as_selected_work) + + @staticmethod + def document_version_links(document_json): + document_id = document_json['document_id'] + version_index = document_json['version_index'] + base = os.environ['KONTROLL_BASE_URL'] + "documents/1.0/document/" + document_id + "/version/" + str(version_index) + return DocumentVersionLinks(**{ + 'document_version': LinkData(**{'url': base}), + 'document_version_metadata': LinkData(**{'url': base + "/metadata"}), + 'document_version_download': LinkData(**{'url': base + "/download"}), + 'document_versions': LinkData(**{'url': base + "/versions"}), + 'document_details': LinkData(**{'url': base + "/details"}) + }) + + def get_download_instructions(self, session_id: UUID, server_context: str, current_user: User) -> SelectedDocuments: + def get_download_instructions_work(tx) -> SelectedDocuments: + cypher = """ + MATCH (ss:Session:Select)-[r1:SELECTED]->(d:Document) + WHERE ss.selection_session = $selection_session + RETURN d AS document + """ + results = tx.run(cypher, selection_session=str(session_id)) + + document_list = list() + for result in results: + document_node = result.get('document') + document_model = self.document_node_to_model(document_node) + document_list.append(document_model) + + selected_documents = SelectedDocuments(**{'server_context': server_context, + 'documents': document_list}) + return selected_documents + + with self.driver.session() as session: + return session.execute_read(get_download_instructions_work) + + + def get_upload_documents(self, upload_session: UUID, current_user: User) -> UploadDocuments: + def get_upload_documents_work(tx) -> UploadDocuments: + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Upload:Session)-[r2:HAS]->(l:Link), + (us)-[r3:CONTAINS]->(d:Document:Temporary) + WHERE u.username = $username + AND us.upload_session = $upload_session + RETURN d AS document + """ + results = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + file_list = list() + for result in results: + file_json = self.node_to_json(result.get('document')) + file_model = FileToUpload(**file_json) + file_list.append(file_model) + + cypher = """ + MATCH (u:User)-[r1:HAS]->(us:Upload:Session)-[r2:HAS]->(l:Link), + WHERE u.username = $username + AND us.upload_session = $upload_session + RETURN + us.server_context AS server_context, + us.callback AS callback, + us.session_callback_timedelta + """ + result = tx.run(cypher, + username=current_user.username, + upload_session=upload_session) + + callback = result.get('callback') + session_callback_timedelta = result.get('session_callback_timedelta') + + upload_documents = UploadDocuments() + upload_documents.server_context = result.get('server_context') + upload_documents.callback.url = result.get('callback') + upload_documents.callback.expires_in = 3500 # difference between now and timedelta + upload_documents.files = file_list + + return upload_documents + + with self.driver.session() as session: + return session.execute_read(get_upload_documents_work) + + def get_document_version(self, document_id: UUID, version_index: int, + current_user: User) -> Union[DocumentVersion, bool]: + def get_document_version_work(tx) -> Union[DocumentVersion, bool]: + + if version_index is None or version_index is False or not isinstance(version_index, int): + version_index_criteria = 'AND d.version_index = $version_index' + else: + version_index_criteria = '' + + cypher = """ + MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) + WHERE u.username = $username + AND d.document_id = $document_id + %s + RETURN d AS document + ORDER by d.version_index DESC + LIMIT 1 + """ % version_index_criteria + + result = tx.run(cypher, + username=current_user.username, + document_id=document_id, + version_index=version_index) + + first = result.single() + if first is None: + print('There were no such document version.') + return False + + document_node = first.get('document') + return self.document_node_to_model(document_node) + + with self.driver.session() as session: + return session.execute_read(get_document_version_work) + + def get_document_version_metadata(self, document_id, version_index, current_user) -> DocumentMetadataEntries: + def get_document_version_metadata_work(tx) -> DocumentMetadataEntries: + + if version_index is None or version_index is False or not isinstance(version_index, int): + version_index_criteria = 'AND d.version_index = $version_index' + else: + version_index_criteria = '' + + cypher = """ + MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) + WHERE u.username = $username + AND d.document_id = $document_id + %s + RETURN d AS document + ORDER by d.version_index DESC + LIMIT 1 + """ % version_index_criteria + + result = tx.run(cypher, + username=current_user.username, + document_id=document_id, + version_index=version_index) + + first = result.single() + if first is None: + print('There were no such document version.') + return False + + document_json = self.node_to_json(first.get('document')) + document_json['creation_date'] = self.bcf_time(document_json['creation_date']) + metadata = ['title', 'version_number', 'creation_date'] + entries = list() + for each_metadata in metadata: + each_metadata_text = each_metadata.replace('_', ' ') + each_metadata_text = each_metadata_text.capitalize() + entry = { + 'name': each_metadata_text, + 'value': [document_json[each_metadata]], + 'data_type': DataType.string + } + entries.append(entry) + return DocumentMetadataEntries(**{'metadata': entries}) + with self.driver.session() as session: + return session.execute_read(get_document_version_metadata_work) + + def get_document_versions(self, document_id, current_user) -> DocumentVersions: + def get_document_versions_work(tx) -> DocumentVersions: + cypher = """ + MATCH (u:User)-[r3:HAS_ACTIONS_ON]->(p:Project)-[r4:CONTAINS]->(d:Document) + WHERE u.username = $username + AND d.document_id = $document_id + RETURN d AS document + """ + results = tx.run(cypher, + username=current_user.username, + document_id=document_id) + document_versions = DocumentVersions({'documents': list()}) + for result in results: + document_json = self.node_to_json(result.get('document')) + document_version = self.get_document_version(document_id, document_json['version_index'], current_user) + document_versions.documents.append(document_version) + return document_versions + with self.driver.session() as session: + return session.execute_read(get_document_versions_work) + + +doc_db = DOCDB(driver) diff --git a/src/opencdeserver/api/app/repository/foundation.py b/src/opencdeserver/api/app/repository/foundation.py new file mode 100644 index 0000000000..803c70a9d1 --- /dev/null +++ b/src/opencdeserver/api/app/repository/foundation.py @@ -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) diff --git a/src/opencdeserver/api/app/requirements.txt b/src/opencdeserver/api/app/requirements.txt new file mode 100644 index 0000000000..9e8eda3bac --- /dev/null +++ b/src/opencdeserver/api/app/requirements.txt @@ -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 + diff --git a/src/opencdeserver/api/app/security/secrets.py b/src/opencdeserver/api/app/security/secrets.py new file mode 100644 index 0000000000..320d70f590 --- /dev/null +++ b/src/opencdeserver/api/app/security/secrets.py @@ -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 diff --git a/src/opencdeserver/api/app/security/secure.py b/src/opencdeserver/api/app/security/secure.py new file mode 100644 index 0000000000..79d98e3c50 --- /dev/null +++ b/src/opencdeserver/api/app/security/secure.py @@ -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 diff --git a/src/opencdeserver/api/app/templates/index.html b/src/opencdeserver/api/app/templates/index.html new file mode 100644 index 0000000000..d00cf17d40 --- /dev/null +++ b/src/opencdeserver/api/app/templates/index.html @@ -0,0 +1,114 @@ + + + + + Kontroll.digital + + + + + + + + +
+
+

kontroll.digital

+
+ + + + +
+ +
+ + + + + + + +
+
+
+ + + diff --git a/src/opencdeserver/api/app/templates/login.html b/src/opencdeserver/api/app/templates/login.html new file mode 100644 index 0000000000..1464b395b2 --- /dev/null +++ b/src/opencdeserver/api/app/templates/login.html @@ -0,0 +1,114 @@ + + + + + Kontroll.digital + + + + + + + + +
+
+

kontroll.digital

+
+ + + + +
+ +
+ + + + + + + +
+
+
+ + + diff --git a/src/opencdeserver/api/app/templates/select_files.html b/src/opencdeserver/api/app/templates/select_files.html new file mode 100644 index 0000000000..338d59ff85 --- /dev/null +++ b/src/opencdeserver/api/app/templates/select_files.html @@ -0,0 +1,225 @@ + + + + + Kontroll.digital + + + + + + + + +
+
+

kontroll.digital

+
+ + + + + + + + + + + + + + + + {% for project in projects %} + + + + + + {% for document in project.documents %} + + + + + + + + + + + + {% endfor %} + + {% endfor %} + + +
IDTitleVersionFile typeSizeCreated
+ +

{{project.name}}

+ + + + + + + + +
{{document.document_id}}{{document.title}}{{document.version_number}}{{document.file_type}}{{document.file_description.size_in_bytes}}{{document.creation_date}}
+ + + + + + + + + +
+
+
+ + diff --git a/src/opencdeserver/api/app/templates/upload_files.html b/src/opencdeserver/api/app/templates/upload_files.html new file mode 100644 index 0000000000..ae58ada357 --- /dev/null +++ b/src/opencdeserver/api/app/templates/upload_files.html @@ -0,0 +1,140 @@ + + + + + Kontroll.digital + + + + + + + + +
+
+

kontroll.digital

+ +
+ + + + + + + + + + + + + {% for document in documents %} + + + + + + + + + + {% endfor %} + + +
IdTitleVersionFile name
{{document.document_id}}{{document.file_name}}
+ + + + + + + + + +
+ + +
+ + + +
+
+
+ + + diff --git a/src/opencdeserver/docker-compose.yml b/src/opencdeserver/docker-compose.yml new file mode 100644 index 0000000000..6dff705874 --- /dev/null +++ b/src/opencdeserver/docker-compose.yml @@ -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 haven’t 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 diff --git a/src/opencdeserver/neo4j/conf/apoc.conf b/src/opencdeserver/neo4j/conf/apoc.conf new file mode 100644 index 0000000000..17e6cca726 --- /dev/null +++ b/src/opencdeserver/neo4j/conf/apoc.conf @@ -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 \ No newline at end of file diff --git a/src/opencdeserver/neo4j/plugins/apoc-5.7.0-core.jar b/src/opencdeserver/neo4j/plugins/apoc-5.7.0-core.jar new file mode 100644 index 0000000000..f0c9f11803 Binary files /dev/null and b/src/opencdeserver/neo4j/plugins/apoc-5.7.0-core.jar differ diff --git a/src/opencdeserver/neo4j/plugins/apoc-5.7.0-extended.jar b/src/opencdeserver/neo4j/plugins/apoc-5.7.0-extended.jar new file mode 100644 index 0000000000..11456253ba Binary files /dev/null and b/src/opencdeserver/neo4j/plugins/apoc-5.7.0-extended.jar differ diff --git a/src/opencdeserver/opencdeserver/bcf/project.json b/src/opencdeserver/opencdeserver/bcf/project.json deleted file mode 100644 index 8033bc5933..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/project.json +++ /dev/null @@ -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" - } - ] -}] -} \ No newline at end of file diff --git a/src/opencdeserver/opencdeserver/bcf/routes.py b/src/opencdeserver/opencdeserver/bcf/routes.py deleted file mode 100644 index f71f14c717..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/routes.py +++ /dev/null @@ -1,650 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -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 "

BCF HOMPAGE

" - - -@bcf.route("/projects/") -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/", 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//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//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//topics/", 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//topics/") -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//topics/", 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//topics/", 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//topics//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//topics//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//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//topics//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//topics//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//topics//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//topics//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//topics//comments/", 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//topics//comments/", 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//topics//comments/", 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//topics//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//topics//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//topics//viewpoints/", 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//topics//viewpoints/", 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//topics//viewpoints//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//topics//viewpoints//bitmaps/", 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//topics//viewpoints//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//topics//viewpoints//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//topics//viewpoints//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//topics//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//topics//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//topics//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//topics//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//topics//document_references/", - 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//topics//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//topics//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//topics//documents/", 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//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//topics//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//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//topics//comments//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() diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/comment_actions.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/comment_actions.json deleted file mode 100644 index 1af06a26dc..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/comment_actions.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "title": "comment_actions", - "required": false, - "type": "array", - "items": { - "type": "string", - "enum": ["update", - "delete"] - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/project_actions.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/project_actions.json deleted file mode 100644 index bf84ca72d5..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/project_actions.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "title": "project_actions", - "required": false, - "type": "array", - "items": { - "type": "string", - "enum": ["update", - "createTopic", - "createDocument"] - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/topic_actions.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/topic_actions.json deleted file mode 100644 index a88e007e11..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/topic_actions.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title": "topic_actions", - "required": false, - "type": "array", - "items": { - "type": "string", - "enum": ["update", - "updateBimSnippet", - "updateRelatedTopics", - "updateDocumentReferences", - "updateFiles", - "createComment", - "createViewpoint", - "delete"] - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/viewpoint_actions.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/viewpoint_actions.json deleted file mode 100644 index 8feb815658..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Action/viewpoint_actions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "required": false, - "type": "array", - "items": { - "enum": ["delete"] - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_GET.json deleted file mode 100644 index 76c2d6a44e..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_GET.json +++ /dev/null @@ -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" - } - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_POST.json deleted file mode 100644 index bb3e60d12a..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_POST.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "title": "comment_POST", - "type": "object", - "properties": { - "guid": { - "type": ["string", - "null"] - }, - "comment": { - "required": true, - "type": "string" - }, - "viewpoint_guid": { - "type": ["string", - "null"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_PUT.json deleted file mode 100644 index 8ceff5afef..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Comment/comment_PUT.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "comment_PUT", - "type": "object", - "properties": { - "comment": { - "required": true, - "type": "string" - }, - "viewpoint_guid": { - "type": ["string", - "null"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Document/document_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Document/document_GET.json deleted file mode 100644 index b8c4025977..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Document/document_GET.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "document_GET", - "type": "object", - "properties": { - "guid": { - "required": true, - "type": "string" - }, - "filename": { - "required": true, - "type": "string" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_GET.json deleted file mode 100644 index 37d649a06e..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_GET.json +++ /dev/null @@ -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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_POST.json deleted file mode 100644 index 0e198490db..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_POST.json +++ /dev/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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_PUT.json deleted file mode 100644 index 2e2dbd4753..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/DocumentReference/document_reference_PUT.json +++ /dev/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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/comment_event_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/comment_event_GET.json deleted file mode 100644 index b623d8ee34..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/comment_event_GET.json +++ /dev/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" - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/event_action.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/event_action.json deleted file mode 100644 index a90c0604bc..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/event_action.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "event_action", - "type": ["object", "null"], - "properties": { - "type": { - "required": true, - "type": "string" - }, - "value": { - "type": ["string", - "null"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/topic_event_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/topic_event_GET.json deleted file mode 100644 index 1192e67ca1..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Events/topic_event_GET.json +++ /dev/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" - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_GET.json deleted file mode 100644 index 55d6b8642f..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_GET.json +++ /dev/null @@ -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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_PUT.json deleted file mode 100644 index 7152d6890c..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/file_PUT.json +++ /dev/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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_display_information.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_display_information.json deleted file mode 100644 index bb808f5dde..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_display_information.json +++ /dev/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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_information.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_information.json deleted file mode 100644 index b85bf59a1b..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_file_information.json +++ /dev/null @@ -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" } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_files_information_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_files_information_GET.json deleted file mode 100644 index 3a85e6a247..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/File/project_files_information_GET.json +++ /dev/null @@ -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" - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_GET.json deleted file mode 100644 index 654ec6fc19..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_GET.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_PUT.json deleted file mode 100644 index 8511a768ea..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/RelatedTopic/related_topic_PUT.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/bim_snippet.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/bim_snippet.json deleted file mode 100644 index 3be8fb8f73..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/bim_snippet.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_GET.json deleted file mode 100644 index 584d041702..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_GET.json +++ /dev/null @@ -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" - } - } - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_POST.json deleted file mode 100644 index 6ae49158fc..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_POST.json +++ /dev/null @@ -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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_PUT.json deleted file mode 100644 index 2b0935eb9a..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Topic/topic_PUT.json +++ /dev/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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_GET.json deleted file mode 100644 index 5373008612..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_GET.json +++ /dev/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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_POST.json deleted file mode 100644 index cd7932dcb3..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/bitmap_POST.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/clipping_plane.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/clipping_plane.json deleted file mode 100644 index e9fad7b61e..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/clipping_plane.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "title": "clipping_plane", - "type": ["object", - "null"], - "properties": { - "location": { - "$ref": "location.json" - }, - "direction": { - "$ref": "direction.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring.json deleted file mode 100644 index 89d3270270..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "coloring", - "type": "object", - "properties": { - "color": { - "type": "string" - }, - "components": { - "$ref": "component_list.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring_GET.json deleted file mode 100644 index c738f20a86..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/coloring_GET.json +++ /dev/null @@ -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" - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component.json deleted file mode 100644 index 49df1c0e1e..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component.json +++ /dev/null @@ -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"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component_list.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component_list.json deleted file mode 100644 index 0ec6d59fe6..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/component_list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "title": "component list", - "type": ["array", "null"], - "items": { - "$ref": "component.json" - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/components.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/components.json deleted file mode 100644 index 66a233ae5f..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/components.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/direction.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/direction.json deleted file mode 100644 index da6e137aee..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/direction.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "direction", - "type": "object", - "properties": { - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/line.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/line.json deleted file mode 100644 index 4f4e0f3af4..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/line.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "title": "line", - "type": ["object", "null"], - "properties": { - "start_point": { - "$ref": "point.json" - }, - "end_point": { - "$ref": "point.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/location.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/location.json deleted file mode 100644 index c38696d938..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/location.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title": "location", - "type": ["object", - "null"], - "properties": { - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/orthogonal_camera.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/orthogonal_camera.json deleted file mode 100644 index f9d69e47b8..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/orthogonal_camera.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/perspective_camera.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/perspective_camera.json deleted file mode 100644 index a3afcdd7ad..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/perspective_camera.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/point.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/point.json deleted file mode 100644 index 19558a426d..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/point.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "title": "point", - "type": "object", - "properties": { - "x": { - "type": "number" - }, - "y": { - "type": "number" - }, - "z": { - "type": "number" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/selection_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/selection_GET.json deleted file mode 100644 index 2296b027f3..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/selection_GET.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_GET.json deleted file mode 100644 index 6235b0add1..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_GET.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "title": "snapshot_GET", - "type": ["object", "null"], - "properties": { - "snapshot_type": { - "type": ["string"], - "enum": ["jpg", "png"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_POST.json deleted file mode 100644 index a349e91cdb..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/snapshot_POST.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "snapshot_POST", - "type": ["object", "null"], - "properties": { - "snapshot_type": { - "type": ["string"], - "enum": ["jpg", "png"] - }, - "snapshot_data": { - "type": ["string"], - "format": "base64" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/view_setup_hints.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/view_setup_hints.json deleted file mode 100644 index 81f8ab7f34..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/view_setup_hints.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_GET.json deleted file mode 100644 index 513c26d4ad..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_GET.json +++ /dev/null @@ -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" - } - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_POST.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_POST.json deleted file mode 100644 index b9408d0d9d..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/viewpoint_POST.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility.json deleted file mode 100644 index 13ea0d08fe..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title": "visibility", - "type": "object", - "properties": { - "default_visibility": { - "type": "boolean", - "default": "false" - }, - "exceptions": { - "$ref": "component_list.json" - }, - "view_setup_hints": { - "$ref": "view_setup_hints.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility_GET.json deleted file mode 100644 index 5fe8714168..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Collaboration/Viewpoint/visibility_GET.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "title": "visibility_GET", - "description": "Schema for visibility GET, BCF REST API.", - "type": "object", - - "properties": { - "visibility": { - "$ref": "visibility.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Project/extensions_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Project/extensions_GET.json deleted file mode 100644 index 92de386640..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Project/extensions_GET.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "title": "extensions_GET", - "type": "object", - "properties": { - "topic_type": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "topic_status": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "topic_label": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "snippet_type": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "priority": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "users": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "stage": { - "required": true, - "type": ["array", - "null"], - "items": { - "type": ["string", - "null"] - } - }, - "project_actions": { - "$ref": "../Collaboration/Action/project_actions.json" - }, - "topic_actions": { - "$ref": "../Collaboration/Action/topic_actions.json" - }, - "comment_actions": { - "$ref": "../Collaboration/Action/comment_actions.json" - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_GET.json deleted file mode 100644 index e239d4e73a..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_GET.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "title": "project_GET", - "description": "Schema for single project GET, BCF REST API.", - "type": "object", - "properties": { - "project_id": { - "type": "string", - "required": true - }, - "name": { - "type": "string", - "required": true - }, - "authorization": { - "type": "object", - "required": false, - "properties": { - "project_actions": { - "$ref": "../Collaboration/Action/project_actions.json" - } - } - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_PUT.json b/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_PUT.json deleted file mode 100644 index 5b317ab276..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/Project/project_PUT.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "title": "project_PUT", - "description": "Schema for project PUT, BCF REST API.", - "type": "object", - "properties": { - "name": { - "type": "string", - "required": true - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/User/user_GET.json b/src/opencdeserver/opencdeserver/bcf/schemas/User/user_GET.json deleted file mode 100644 index 873129c155..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/User/user_GET.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "user_GET", - "type": "object", - "properties": { - "id": { - "required": true, - "type": "string" - }, - "name": { - "type": ["string", - "null"] - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/schemas/error.json b/src/opencdeserver/opencdeserver/bcf/schemas/error.json deleted file mode 100644 index 66a5550975..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/schemas/error.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "title": "Error", - "description": "Schema for error, BCF REST API.", - "type": "object", - "properties": { - "message": { - "type": "string", - "required": true - } - } -} diff --git a/src/opencdeserver/opencdeserver/bcf/success.txt b/src/opencdeserver/opencdeserver/bcf/success.txt deleted file mode 100644 index 49ca5603fd..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/success.txt +++ /dev/null @@ -1 +0,0 @@ -Your request was successful \ No newline at end of file diff --git a/src/opencdeserver/opencdeserver/bcf/templates/base.html b/src/opencdeserver/opencdeserver/bcf/templates/base.html deleted file mode 100644 index 59ae9b37da..0000000000 --- a/src/opencdeserver/opencdeserver/bcf/templates/base.html +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/src/opencdeserver/opencdeserver/foundation/forms.py b/src/opencdeserver/opencdeserver/foundation/forms.py deleted file mode 100644 index 2f86536b8d..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/forms.py +++ /dev/null @@ -1,73 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo -from wtforms import ValidationError -from .models import User - - -class RegisterForm(FlaskForm): - def validate_username(self, username_to_check): - user = User.query.filter_by(username=username_to_check.data).first() - if user: - raise ValidationError( - "Username already exists! Please try a different username" - ) - - def validate_email_address(self, email_address_to_check): - email_address = User.query.filter_by( - email_address=email_address_to_check.data - ).first() - if email_address: - raise ValidationError("Email Address already exists!") - - username = StringField( - label="User Name:", validators=[Length(min=2, max=30), DataRequired()] - ) - email_address = StringField( - label="Email Address:", validators=[Email(), DataRequired()] - ) - password1 = PasswordField( - label="Password:", validators=[Length(min=6), DataRequired()] - ) - password2 = PasswordField( - label="Confirm Password:", validators=[EqualTo("password1"), DataRequired()] - ) - submit = SubmitField(label="Create Account") - - -class LoginForm(FlaskForm): - username = StringField(label="User Name:", validators=[DataRequired()]) - password = PasswordField(label="Password:", validators=[DataRequired()]) - submit = SubmitField(label="Sign in") - - -class OauthForm(FlaskForm): - client_name = StringField(label="Client Name:", validators=[DataRequired()]) - # client_uri = StringField(label="Client URI:", validators=[DataRequired()]) - grant_types = StringField(label="Grant Types:", validators=[DataRequired()]) - # redirect_uris = StringField(label="Redirect URIs:", validators=[DataRequired()]) - response_types = StringField(label="Response Types:", validators=[DataRequired()]) - scope = StringField(label="Scope:", validators=[DataRequired()]) - # token_endpoint_auth_method = StringField( - # label="Token Endpoint Auth Method:", validators=[DataRequired()] - # ) - submit = SubmitField(label="Create OAuth") diff --git a/src/opencdeserver/opencdeserver/foundation/models.py b/src/opencdeserver/opencdeserver/foundation/models.py deleted file mode 100644 index db451e4022..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/models.py +++ /dev/null @@ -1,88 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -from run import db, bcrypt, login_manager -import time -from authlib.integrations.sqla_oauth2 import ( - OAuth2ClientMixin, - OAuth2AuthorizationCodeMixin, - OAuth2TokenMixin, -) -from flask_login import UserMixin - - -@login_manager.user_loader -def load_user(user_id): - return User.query.get(int(user_id)) - - -class User(db.Model, UserMixin): - id = db.Column(db.Integer(), primary_key=True) - username = db.Column(db.String(50), unique=True, nullable=False) - password_hash = db.Column(db.String(80), nullable=False) - email_address = db.Column(db.String(50), unique=True, nullable=False) - - def __str__(self): - return f"{self.username} {self.email_address}" - - def get_user_id(self): - return self.id - - @property - def password(self): - return self.password - - @password.setter - def password(self, plain_text_password): - self.password_hash = bcrypt.generate_password_hash(plain_text_password).decode( - "utf-8" - ) - - def check_password(self, attempted_password): - return bcrypt.check_password_hash(self.password_hash, attempted_password) - - -class OAuth2Client(db.Model, OAuth2ClientMixin): - __tablename__ = "oauth2_client" - - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")) - user = db.relationship("User", lazy=True) - - -class OAuth2AuthorizationCode(db.Model, OAuth2AuthorizationCodeMixin): - __tablename__ = "oauth2_code" - - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")) - user = db.relationship("User", lazy=True) - - -class OAuth2Token(db.Model, OAuth2TokenMixin): - __tablename__ = "oauth2_token" - - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")) - user = db.relationship("User", lazy=True) - - def is_refresh_token_active(self): - if self.revoked: - return False - expires_at = self.issued_at + self.expires_in * 2 - return expires_at >= time.time() diff --git a/src/opencdeserver/opencdeserver/foundation/oauth2.py b/src/opencdeserver/opencdeserver/foundation/oauth2.py deleted file mode 100644 index bc69b28c0e..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/oauth2.py +++ /dev/null @@ -1,121 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -from authlib.integrations.flask_oauth2 import ( - AuthorizationServer, - ResourceProtector, -) -from authlib.integrations.sqla_oauth2 import ( - create_query_client_func, - create_save_token_func, - create_revocation_endpoint, - create_bearer_token_validator, -) -from authlib.oauth2.rfc6749 import grants -from authlib.oauth2.rfc7636 import CodeChallenge -from .models import db, User -from .models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token - - -class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): - TOKEN_ENDPOINT_AUTH_METHODS = [ - "client_secret_basic", - "client_secret_post", - "none", - ] - - def save_authorization_code(self, code, request): - code_challenge = request.data.get("code_challenge") - code_challenge_method = request.data.get("code_challenge_method") - auth_code = OAuth2AuthorizationCode( - code=code, - client_id=request.client.client_id, - redirect_uri=request.redirect_uri, - scope=request.scope, - user_id=request.user.id, - code_challenge=code_challenge, - code_challenge_method=code_challenge_method, - ) - db.session.add(auth_code) - db.session.commit() - return auth_code - - def query_authorization_code(self, code, client): - auth_code = OAuth2AuthorizationCode.query.filter_by( - code=code, client_id=client.client_id - ).first() - if auth_code and not auth_code.is_expired(): - return auth_code - - def delete_authorization_code(self, authorization_code): - db.session.delete(authorization_code) - db.session.commit() - - def authenticate_user(self, authorization_code): - return User.query.get(authorization_code.user_id) - - -class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant): - def authenticate_user(self, username, password): - user = User.query.filter_by(username=username).first() - if user is not None and user.check_password(password): - return user - - -class RefreshTokenGrant(grants.RefreshTokenGrant): - def authenticate_refresh_token(self, refresh_token): - token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first() - if token and token.is_refresh_token_active(): - return token - - def authenticate_user(self, credential): - return User.query.get(credential.user_id) - - def revoke_old_credential(self, credential): - credential.revoked = True - db.session.add(credential) - db.session.commit() - - -query_client = create_query_client_func(db.session, OAuth2Client) -save_token = create_save_token_func(db.session, OAuth2Token) -authorization = AuthorizationServer( - query_client=query_client, - save_token=save_token, -) -require_oauth = ResourceProtector() - - -def config_oauth(app): - authorization.init_app(app) - - # support all grants - authorization.register_grant(grants.ImplicitGrant) - authorization.register_grant(grants.ClientCredentialsGrant) - authorization.register_grant(AuthorizationCodeGrant, [CodeChallenge(required=True)]) - authorization.register_grant(PasswordGrant) - authorization.register_grant(RefreshTokenGrant) - - # support revocation - revocation_cls = create_revocation_endpoint(db.session, OAuth2Token) - authorization.register_endpoint(revocation_cls) - - # protect resource - bearer_cls = create_bearer_token_validator(db.session, OAuth2Token) - require_oauth.register_token_validator(bearer_cls()) diff --git a/src/opencdeserver/opencdeserver/foundation/routes.py b/src/opencdeserver/opencdeserver/foundation/routes.py deleted file mode 100644 index 4f817b3d70..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/routes.py +++ /dev/null @@ -1,321 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -from flask.blueprints import Blueprint -from flask import render_template, redirect, url_for, flash, request -from werkzeug import datastructures -from .forms import RegisterForm, LoginForm, OauthForm -from flask_login import login_user, logout_user, login_required, current_user -from .models import User, OAuth2Client, OAuth2AuthorizationCode, OAuth2Token -import base64 -from werkzeug.security import gen_salt -import time -import urllib -import json -from run import db, app - -foundation_obj = Blueprint( - "foundation_obj", - __name__, - template_folder="templates", -) - - -def split_by_crlf(s): - return [v for v in s.splitlines() if v] - - -@foundation_obj.route("/") -def homepage(): - return render_template("index.html") - # return "homepage" - - -@foundation_obj.route("/register", methods=["GET", "POST"]) -def register_page(): - form = RegisterForm() - if form.validate_on_submit(): - user_to_create = User( - username=form.username.data, - email_address=form.email_address.data, - password=form.password1.data, - ) - - db.session.add(user_to_create) - db.session.commit() - login_user(user_to_create) - flash( - f"Account created successfully! {user_to_create.username}", - category="success", - ) - return redirect(url_for("foundation_obj.homepage")) - if form.errors != {}: - for err_msg in form.errors.values(): - flash( - f"There was an error with creating a user: {err_msg}", category="danger" - ) - - return render_template("register.html", form=form) - - -@foundation_obj.route("/createclient", methods=["GET", "POST"]) -@login_required -def create_client(): - grants = [ - "AuthorizationCodeGrant", - "ImplicitGrant", - "ResourceOwnerPasswordCredentialsGrant", - "ClientCredentialsGrant", - "RefreshTokenGrant", - ] - form = OauthForm() - if form.validate_on_submit(): - client_id = gen_salt(24) - client_id_issued_at = int(time.time()) - - client = OAuth2Client( - client_id=client_id, - client_id_issued_at=client_id_issued_at, - user_id=current_user.id, - ) - client_metadata = { - "client_name": form.client_name.data, - "grant_types": split_by_crlf(form.grant_types.data), - "response_types": split_by_crlf(form.response_types.data), - "scope": form.scope.data, - "token_endpoint_auth_method": "client_secret_basic", - } - client.client_secret = gen_salt(48) - - client.set_client_metadata(client_metadata) - db.session.add(client) - db.session.commit() - flash( - "Oauth Client Created Successfully", - category="info", - ) - clients = OAuth2Client.query.filter_by(user_id=current_user.id).all() - for client in clients: - print(client.client_info) - print(client.client_metadata) - return render_template("clientdata.html", user=current_user.id, clients=clients) - return render_template("createclient.html", form=form, grants=grants) - - -@foundation_obj.route("/login", methods=["GET", "POST"]) -def login_page(): - form = LoginForm() - if form.validate_on_submit(): - attempted_user = User.query.filter_by(username=form.username.data).first() - if attempted_user and attempted_user.check_password( - attempted_password=form.password.data - ): - login_user(attempted_user) - return redirect(url_for("foundation_obj.homepage")) - else: - flash( - "Invalid Credentials", - category="danger", - ) - - return render_template("login.html", form=form) - - -@foundation_obj.route("/logout") -def logoutpage(): - logout_user() - flash("You have been logged out!", category="info") - return redirect(url_for("foundation_obj.homepage")) - - -@foundation_obj.route("/foundation/1.0/auth") -def foundation_auth(): - data = { - "oauth2_auth_url": "http://127.0.0.1:5000/oauth/authorize", - "oauth2_token_url": "http://127.0.0.1:5000/oauth/token", - "supported_oauth2_flows": ["authorization_code"], - } - response = app.response_class( - response=json.dumps(data), status=200, mimetype="application/json" - ) - return response - - -@foundation_obj.route("/foundation/versions") -def foundation_versions(): - Body = { - "versions": [ - { - "api_id": "opencde-foundation", - "version_id": "1.0", - "detailed_version": "https://github.com/BuildingSMART/opencde-foundation-API/tree/v1.0", - }, - # { - # "api_id": "bcf", - # "version_id": "2.1", - # "detailed_version": "https://github.com/buildingSMART/BCF-API/tree/release_2_1", - # "api_base_url": "http://127.0.0.1:5000/bcf/2.1" - # }, - { - "api_id": "bcf", - "version_id": "3.0", - "detailed_version": "https://github.com/buildingSMART/BCF-API/tree/release_3_0", - "api_base_url": "http://127.0.0.1:5000/bcf/3.0", - }, - ] - } - response = app.response_class( - response=json.dumps(Body), status=200, mimetype="application/json" - ) - return response - - -@foundation_obj.route("/outh/login", methods=["GET", "POST"]) -def oauth_login(): - form = LoginForm() - if form.validate_on_submit(): - attempted_user = User.query.filter_by(username=form.username.data).first() - if attempted_user and attempted_user.check_password( - attempted_password=form.password.data - ): - login_user(attempted_user) - flash( - "Invalid Credentials", - category="info", - ) - client_id = request.args.get("client_id") - redirect_uri = request.args.get("redirect_uri") - state = request.args.get("state") - return redirect( - url_for( - "foundation_obj.authorize", - client_id=client_id, - redirect_uri=redirect_uri, - state=state, - ) - ) - else: - flash( - "Invalid Credentials", - category="danger", - ) - return render_template("ologin.html", form=form) - - -@foundation_obj.route("/oauth/authorize", methods=["GET", "POST"]) -def authorize(): - client_id = request.args.get("client_id") - redirect_uri = request.args.get("redirect_uri") - state = request.args.get("state") - if current_user.is_anonymous: - flash("Please Login !", category="info") - query = request.query_string - return redirect( - url_for( - "foundation_obj.oauth_login", - client_id=client_id, - redirect_uri=redirect_uri, - state=state, - ) - ) - else: - try: - - user = current_user.id - flash(message=redirect_uri, category="warning") - client = OAuth2Client.query.filter_by(client_id=client_id).first() - if client: - if client.user_id == user: - code = gen_salt(48) - OauthCode = OAuth2AuthorizationCode( - client_id=client_id, - redirect_uri=redirect_uri, - user_id=user, - code=code, - response_type="code", - ) - db.session.add(OauthCode) - db.session.commit() - query = urllib.parse.urlencode( - { - "code": OauthCode.code, - "state": state, - } - ) - return redirect(f"{redirect_uri}?{query}") - else: - flash( - "You are not authorized to access this client", - category="danger", - ) - else: - flash("Client not found", category="danger") - except Exception as e: - flash(e, category="danger") - return render_template("oauth.html") - - -@foundation_obj.route("/oauth/token", methods=["POST"]) -def issue_token(): - try: - code = request.form["code"] - Headers = str.split(request.headers["Authorization"]) - decode_header = base64.b64decode(Headers[1]).decode("utf-8") - creds = decode_header.split(":") - client_id = creds[0] - # print(code, Headers, decode_header, creds, client_id) - auth_code = OAuth2AuthorizationCode.query.filter_by(code=code).first() - if auth_code: - if auth_code.client_id == client_id: - access_token = gen_salt(48) - refresh_token = gen_salt(48) - expires_in = 3600 - OauthToken = OAuth2Token( - client_id=auth_code.client_id, - user_id=auth_code.user_id, - access_token=access_token, - refresh_token=refresh_token, - expires_in=expires_in, - scope=auth_code.scope, - token_type="Bearer", - ) - db.session.add(OauthToken) - db.session.commit() - query = { - "access_token": access_token, - "refresh_token": refresh_token, - "expires_in": expires_in, - } - response = app.response_class( - response=json.dumps(query), - status=200, - mimetype="application/json", - ) - print(query) - return response - else: - flash( - "You are not authorized to access this client", - category="danger", - ) - else: - flash("Client not found", category="danger") - except Exception as e: - flash(e, category="danger") - return render_template("oauth.html") diff --git a/src/opencdeserver/opencdeserver/foundation/templates/base.html b/src/opencdeserver/opencdeserver/foundation/templates/base.html deleted file mode 100644 index 70cb6009c5..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/base.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - {% block title %} {% endblock %} - - - - {% with messages = get_flashed_messages(with_categories=true) %} {% if - messages %} {% for category, message in messages %} -
- - {{ message }} -
- {% endfor %} {% endif %} {% endwith %} {% block content %} {% endblock %} - - - - - - - - - - diff --git a/src/opencdeserver/opencdeserver/foundation/templates/clientdata.html b/src/opencdeserver/opencdeserver/foundation/templates/clientdata.html deleted file mode 100644 index c78d37c989..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/clientdata.html +++ /dev/null @@ -1,36 +0,0 @@ - -{%extends 'base.html'%} {%block content%} {% if user %} - -
Logged in as {{user}}
- -{% for client in clients %} -
-{{ client.client_info|tojson }}
-{{ client.client_metadata|tojson }}
-
-
-{% endfor %} {% else %} -
Not logged in
-{% endif %} {% endblock %} diff --git a/src/opencdeserver/opencdeserver/foundation/templates/createclient.html b/src/opencdeserver/opencdeserver/foundation/templates/createclient.html deleted file mode 100644 index b423982721..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/createclient.html +++ /dev/null @@ -1,47 +0,0 @@ - -{%extends 'base.html' %} {%block title %} {% endblock%} {%block content%} - -
-
- {{form.hidden_tag()}} -

- Register -

- {{form.client_name.label()}} {{form.client_name(class="form-control", - placeholder="Client_name")}} - - {{form.grant_types.label()}} {{form.grant_types(class="form-control", - placeholder="Grant_Types")}} {{form.response_types.label()}} - {{form.response_types(class="form-control", - placeholder="Response_Types")}} {{form.scope.label()}} - {{form.scope(class="form-control", placeholder="Scope")}} -
- {{form.submit(class="btn btn-lg btn-primary btn-block" ,value="Create - Oauth Credentials")}} -
-
- -{% endblock %} diff --git a/src/opencdeserver/opencdeserver/foundation/templates/index.html b/src/opencdeserver/opencdeserver/foundation/templates/index.html deleted file mode 100644 index b32168adf5..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - Document - - - Home Page - - diff --git a/src/opencdeserver/opencdeserver/foundation/templates/login.html b/src/opencdeserver/opencdeserver/foundation/templates/login.html deleted file mode 100644 index 896fff4b07..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/login.html +++ /dev/null @@ -1,46 +0,0 @@ - -{% extends 'base.html' %} {% block title %}{% endblock %} {% block content %} - -
- -
- -{% endblock %} diff --git a/src/opencdeserver/opencdeserver/foundation/templates/oauth.html b/src/opencdeserver/opencdeserver/foundation/templates/oauth.html deleted file mode 100644 index 544542e04b..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/oauth.html +++ /dev/null @@ -1,20 +0,0 @@ - -{%extends "base.html"%} {%block content%} this is Oauth page {%endblock%} diff --git a/src/opencdeserver/opencdeserver/foundation/templates/ologin.html b/src/opencdeserver/opencdeserver/foundation/templates/ologin.html deleted file mode 100644 index 6f06cf5871..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/ologin.html +++ /dev/null @@ -1,47 +0,0 @@ - -{% extends 'base.html' %} {% block title %}{% endblock %} {% block content %} - -
- -
- -{% endblock %} diff --git a/src/opencdeserver/opencdeserver/foundation/templates/register.html b/src/opencdeserver/opencdeserver/foundation/templates/register.html deleted file mode 100644 index 426d5c4318..0000000000 --- a/src/opencdeserver/opencdeserver/foundation/templates/register.html +++ /dev/null @@ -1,49 +0,0 @@ - -{%extends 'base.html' %} {%block title %} {% endblock%} {%block content%} - -
-
- {{form.hidden_tag()}} -

- Register -

- {{form.username.label()}} {{form.username(class="form-control" - ,placeholder="Username")}} {{form.email_address.label()}} - {{form.email_address(class="form-control" ,placeholder="Email Address")}} - {{form.password1.label()}} - {{form.password1(class="form-control",placeholder="Password")}} - {{form.password2.label()}} {{form.password2(class="form-control" - ,placeholder="Confirm Password")}} -
-
-
Already have an account?
- Login -
- {{form.submit(class="btn btn-lg btn-primary btn-block" - ,value="Register")}} -
-
- -{% endblock %} diff --git a/src/opencdeserver/opencdeserver/run.py b/src/opencdeserver/opencdeserver/run.py deleted file mode 100644 index a4cc214606..0000000000 --- a/src/opencdeserver/opencdeserver/run.py +++ /dev/null @@ -1,44 +0,0 @@ - -# OpenCDE - OpenCDE Python implementation -# Copyright (C) 2021 Prabhat Singh -# -# 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 . - -from flask import Flask -from flask_sqlalchemy import SQLAlchemy -from flask_login import LoginManager -from flask_bcrypt import Bcrypt - - -app = Flask(__name__) -db = SQLAlchemy(app) -login_manager = LoginManager(app) -bcrypt = Bcrypt(app) -app.config["SECRET_KEY"] = "f613729206685405cde0e388" -app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///sqlite.db" -login_manager.login_view = "foundation_obj.login_page" -login_manager.login_message_category = "info" - -from foundation.routes import foundation_obj -from bcf.routes import bcf - - -app.register_blueprint(foundation_obj) -app.register_blueprint(bcf) - - -if __name__ == "__main__": - app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/src/opencdeserver/requirements.txt b/src/opencdeserver/requirements.txt deleted file mode 100644 index 326d772462..0000000000 --- a/src/opencdeserver/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -Authlib==0.15.4 -email-validator==1.1.3 -Flask==2.3.2 -Flask-Login==0.5.0 -Flask-SQLAlchemy==2.5.1 -Flask-WTF==0.15.1 -Flask-Bcrypt==0.7.1 -bcrypt==3.2.0 -flask-expects-json==1.6.0 \ No newline at end of file diff --git a/src/opencdeserver/restart.sh b/src/opencdeserver/restart.sh new file mode 100644 index 0000000000..41a34a3562 --- /dev/null +++ b/src/opencdeserver/restart.sh @@ -0,0 +1,6 @@ +#!/bin/bash +docker stop kontroll_fastapi +docker rm -v kontroll_fastapi +docker image rm kontroll_fastapi:latest +docker-compose up -d +docker logs kontroll_fastapi --follow