diff --git a/.bootstraprc b/.bootstraprc index e579effcaed034a57ec58ed2b2205a61833e0952..60d67fc5d59cf90493f80080cc920cfc898b4981 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -43,10 +43,10 @@ "collapse": true, "dropdown": true, "modal": true, + "popover": true, "scrollspy": true, "tab": true, "tooltip": true, "util": true, - "popover": true, } } diff --git a/.gitignore b/.gitignore index 19a8e42a0a8c3ef0460538c32a683072039fc202..a991d42fe1f8fe5841e80bc0816af36d4099e6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ SCIPOST_JOURNALS UPLOADS /docs/_build +/docs/*/_build/* /docs/local_files/ /local_files/ /static/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..be3f7b28e564e7dd05eaf59d64adba1a4065ac0e --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<https://www.gnu.org/licenses/>. diff --git a/README.md b/README.md index 68684ffab21961cff447deb30eb129a90eb61f5b..3fbb5d17cc84c8f9448cf52252c0585573fda56b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # SciPost -The complete scientific publication portal +This repository carries the entire codebase for the [scipost.org](https://scipost.org) scientific publication portal. + +## Project organization +Development work for SciPost is headed by [Jean-Sébastien Caux](https://jscaux.org) and Jorran de Wit. The development team can be contacted at [techsupport@scipost.org](mailto:techsupport@scipost.org). + +## License +This codebase is released under the terms of the GNU Affero General Public License (Version 3, 19 November 2007). ## Dependencies SciPost is written in Python 3.5 using Django 1.11 and requires PostgreSQL 9.4 or @@ -173,7 +179,7 @@ The documentation is saved in the local database as a Project with name `SciPost To update the docs, simply run ```shell -(scipostenv) $ python3 ../manage.py updatedoc -b scipost +(scipostenv) $ ./manage.py updatedoc -b scipost ``` The documentation is then viewable by navigating to `docs/`. @@ -215,7 +221,7 @@ Any regular method or class based view may be used together with the builtin wys ```python from django.views.generic.edit import UpdateView -from mails.mixins import MailEditorMixin +from mails.views import MailEditorMixin class AnyUpdateView(MailEditorMixin, UpdateView): mail_code = '<any_valid_mail_code>' diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 26938f784a87c2a4e529aff70628aa67e7544a51..67b257595f93db434a1e88773f11eedcf06249fe 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -140,9 +140,9 @@ SHELL_PLUS_POST_IMPORTS = ( ('submissions.factories', ('SubmissionFactory', 'EICassignedSubmissionFactory')), ('commentaries.factories', ('EmptyCommentaryFactory', - 'VettedCommentaryFactory', + 'CommentaryFactory', 'UnvettedCommentaryFactory', - 'UnpublishedVettedCommentaryFactory',)), + 'UnpublishedCommentaryFactory',)), ('scipost.factories', ('ContributorFactory')), ) diff --git a/affiliations/factories.py b/affiliations/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..c01e316b1598c30d0bd732341c65464dbd1a64f1 --- /dev/null +++ b/affiliations/factories.py @@ -0,0 +1,26 @@ +import factory + +from .constants import INSTITUTION_TYPES +from .models import Institution, Affiliation + + +class InstitutionFactory(factory.django.DjangoModelFactory): + name = factory.Faker('company') + acronym = factory.lazy_attribute(lambda o: o.name[:16]) + country = factory.Faker('country_code') + type = factory.Iterator(INSTITUTION_TYPES, getter=lambda c: c[0]) + + class Meta: + model = Institution + django_get_or_create = ('name',) + + +class AffiliationFactory(factory.django.DjangoModelFactory): + institution = factory.SubFactory('affiliations.factories.InstitutionFactory') + contributor = factory.SubFactory('scipost.factories.ContributorFactory') + begin_date = factory.Faker('date_this_decade') + end_date = factory.Faker('future_date', end_date="+2y") + + class Meta: + model = Affiliation + django_get_or_create = ('institution', 'contributor') diff --git a/affiliations/forms.py b/affiliations/forms.py index 774a2e6a7895455701b1fbc7652c79cc6c4aa205..c9161718f72d98032bd66e618338be0453c398bf 100644 --- a/affiliations/forms.py +++ b/affiliations/forms.py @@ -1,6 +1,5 @@ from django import forms from django.forms import BaseModelFormSet, modelformset_factory -# from django.db.models import F from django_countries import countries from django_countries.fields import LazyTypedChoiceField @@ -66,7 +65,7 @@ class AffiliationForm(forms.ModelForm): return affiliation -class AffiliationsFormSet(BaseModelFormSet): +class BaseAffiliationsFormSet(BaseModelFormSet): """ This formset helps update the Institutions for the Contributor at specific time periods. """ @@ -93,7 +92,7 @@ class AffiliationsFormSet(BaseModelFormSet): AffiliationsFormset = modelformset_factory(Affiliation, form=AffiliationForm, can_delete=True, - formset=AffiliationsFormSet, extra=0) + formset=BaseAffiliationsFormSet, extra=0) class InstitutionMergeForm(forms.ModelForm): diff --git a/colleges/factories.py b/colleges/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..dc1a829b8427c6ffb2358776aabfb5e2bae15f67 --- /dev/null +++ b/colleges/factories.py @@ -0,0 +1,22 @@ +import factory + +from scipost.models import Contributor + +from .models import Fellowship + + +class BaseFellowshipFactory(factory.django.DjangoModelFactory): + contributor = factory.Iterator(Contributor.objects.all()) + start_date = factory.Faker('date_this_year') + until_date = factory.Faker('date_between', start_date="now", end_date="+2y") + + guest = factory.Faker('boolean', chance_of_getting_true=10) + + class Meta: + model = Fellowship + django_get_or_create = ('contributor', 'start_date') + abstract = True + + +class FellowshipFactory(BaseFellowshipFactory): + pass diff --git a/docs/users/_build/json/last_build b/colleges/management/__init__.py similarity index 100% rename from docs/users/_build/json/last_build rename to colleges/management/__init__.py diff --git a/colleges/management/commands/__init__.py b/colleges/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/colleges/management/commands/create_fellowships.py b/colleges/management/commands/create_fellowships.py new file mode 100644 index 0000000000000000000000000000000000000000..b28ad5069950659c47e69e2794859309079bed2b --- /dev/null +++ b/colleges/management/commands/create_fellowships.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from colleges import factories + + +class Command(BaseCommand): + help = 'Create random Fellowships objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Fellowships to add') + + def handle(self, *args, **kwargs): + self.create_fellowships(kwargs['number']) + + def create_fellowships(self, n): + factories.FellowshipFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Fellowships.'.format(n=n))) diff --git a/commentaries/admin.py b/commentaries/admin.py index 93aee6375bdb1b36584042765d5e5220d7d1ca0e..f303ebcfefa5c47f18b939ac1f180aff8412aed4 100644 --- a/commentaries/admin.py +++ b/commentaries/admin.py @@ -29,4 +29,5 @@ class CommentaryAdmin(admin.ModelAdmin): date_hierarchy = 'latest_activity' form = CommentaryAdminForm + admin.site.register(Commentary, CommentaryAdmin) diff --git a/commentaries/factories.py b/commentaries/factories.py index c908621441bff8715a7bbaf5cce7cd3833994445..0e0f2df751417dc915d3e88002a2cb8e085d4a0c 100644 --- a/commentaries/factories.py +++ b/commentaries/factories.py @@ -8,31 +8,31 @@ from common.helpers import random_arxiv_identifier_with_version_number, random_e from .constants import COMMENTARY_TYPES from .models import Commentary -from faker import Faker - -class CommentaryFactory(factory.django.DjangoModelFactory): +class BaseCommentaryFactory(factory.django.DjangoModelFactory): class Meta: model = Commentary + django_get_or_create = ('pub_DOI', 'arxiv_identifier') + abstract = True requested_by = factory.Iterator(Contributor.objects.all()) + vetted = True + vetted_by = factory.Iterator(Contributor.objects.all()) type = factory.Iterator(COMMENTARY_TYPES, getter=lambda c: c[0]) discipline = factory.Iterator(SCIPOST_DISCIPLINES, getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) - title = factory.Faker('text') + title = factory.Faker('sentence') pub_DOI = factory.Sequence(lambda n: random_external_doi()) - arxiv_identifier = factory.Sequence(lambda n: random_arxiv_identifier_with_version_number()) + arxiv_identifier = factory.Sequence(lambda n: random_arxiv_identifier_with_version_number('1')) author_list = factory.Faker('name') pub_abstract = factory.Faker('text') - pub_date = factory.Faker('date') - arxiv_link = factory.Faker('uri') - pub_abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) + pub_date = factory.Faker('date_this_decade') + pub_abstract = factory.Faker('paragraph') - @factory.post_generation - def arxiv_link(self, create, extracted, **kwargs): - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier - self.arxiv_or_DOI_string = self.arxiv_identifier + arxiv_link = factory.lazy_attribute(lambda o: 'https://arxiv.org/abs/%s' % o.arxiv_identifier) + arxiv_or_DOI_string = factory.lazy_attribute(lambda o: ( + o.arxiv_identifier if o.arxiv_identifier else o.pub_DOI)) @factory.post_generation def create_urls(self, create, extracted, **kwargs): @@ -40,27 +40,45 @@ class CommentaryFactory(factory.django.DjangoModelFactory): @factory.post_generation def add_authors(self, create, extracted, **kwargs): - contributors = list(Contributor.objects.order_by('?') - .exclude(pk=self.requested_by.pk).all()[:4]) + contributors = Contributor.objects.order_by('?').exclude(pk=self.requested_by.pk)[:4] self.author_list = ', '.join( - ['%s %s' % (contrib.user.first_name, - contrib.user.last_name) for contrib in contributors]) - self.authors.add(*contributors) + ['%s %s' % (contrib.user.first_name, contrib.user.last_name) + for contrib in contributors]) + if create: + self.authors.add(*contributors) -class VettedCommentaryFactory(CommentaryFactory): - vetted = True - vetted_by = factory.Iterator(Contributor.objects.all()) + @factory.post_generation + def set_journal_data(self, create, extracted, **kwargs): + if not self.pub_DOI: + return + data = self.pub_DOI.split('/')[1].split('.') + self.journal = data[0] + self.volume = data[1] + self.pages = data[2] -class UnpublishedVettedCommentaryFactory(VettedCommentaryFactory): - pub_DOI = '' +class CommentaryFactory(BaseCommentaryFactory): + pass -class UnvettedCommentaryFactory(CommentaryFactory): + +class UnvettedCommentaryFactory(BaseCommentaryFactory): vetted = False + vetted_by = None -class UnvettedArxivPreprintCommentaryFactory(CommentaryFactory): - vetted = False +class UnpublishedCommentaryFactory(BaseCommentaryFactory): pub_DOI = '' + pub_date = None + + +class UnvettedUnpublishedCommentaryFactory(UnpublishedCommentaryFactory): + vetted = False + vetted_by = None + + +class PublishedCommentaryFactory(BaseCommentaryFactory): + arxiv_identifier = '' + arxiv_link = '' + arxiv_or_DOI_string = factory.lazy_attribute(lambda o: o.pub_DOI) diff --git a/commentaries/management/__init__.py b/commentaries/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/commentaries/management/commands/__init__.py b/commentaries/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/commentaries/management/commands/create_commentaries.py b/commentaries/management/commands/create_commentaries.py new file mode 100644 index 0000000000000000000000000000000000000000..1353a86dd1794882a4587d0f916e98f7ddd9ad33 --- /dev/null +++ b/commentaries/management/commands/create_commentaries.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from commentaries import factories + + +class Command(BaseCommand): + help = 'Create random Commentaries objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Commentaries to add') + + def handle(self, *args, **kwargs): + self.create_commentaries(kwargs['number']) + + def create_commentaries(self, n): + factories.CommentaryFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Commentaries.'.format(n=n))) diff --git a/commentaries/templates/commentaries/_commentary_card_content.html b/commentaries/templates/commentaries/_commentary_card_content.html index 7c5954005dbba4b8a85a7312f0684392da50c232..8bb9d61f56cffda63f510a8a9f07dadb53027349 100644 --- a/commentaries/templates/commentaries/_commentary_card_content.html +++ b/commentaries/templates/commentaries/_commentary_card_content.html @@ -1,11 +1,11 @@ -<div class="card-body"> - <h3 class="card-title"> - <a href="{% url 'commentaries:commentary' commentary.arxiv_or_DOI_string %}">{{ commentary.title }}</a> - </h3> - <p class="mt-0 mb-3"> - by {{ commentary.author_list }}{% if commentary.type == 'published' %}, {{ commentary.journal }} {{ commentary.volume }}, {{ commentary.pages }}{% elif commentary.type == 'preprint' %} · <a href="{{ commentary.arxiv_link }}">{{ commentary.arxiv_link }}</a>{% endif %} - </p> - <p class="card-text text-muted"> - {% if commentary.pub_date %}(published {{ commentary.pub_date }}) · {% endif %}latest activity: {{ commentary.latest_activity }} - </p> +<div class="card-body px-0"> + <div class="li commentary"> + <h3 class="title"><a href="{{ commentary.get_absolute_url }}">{{ commentary.title }}</a></h3> + <p class="authors"> + by {{ commentary.author_list }}{% if commentary.type == 'published' %}, {{ commentary.journal }} {{ commentary.volume }}, {{ commentary.pages }}{% elif commentary.type == 'preprint' %} · <a href="{{ commentary.arxiv_link }}">{{ commentary.arxiv_link }}</a>{% endif %} + </p> + <p class="meta"> + {% if commentary.pub_date %}(published {{ commentary.pub_date }}) · {% endif %}latest activity: {{ commentary.latest_activity }} + </p> + </div> </div> diff --git a/commentaries/templates/commentaries/_commentary_summary.html b/commentaries/templates/commentaries/_commentary_summary.html index 7567fea0d99cbf2d19e9d17abae4c852be517e06..8c817cca6a218f64e0473e78980e909e5a220ff0 100644 --- a/commentaries/templates/commentaries/_commentary_summary.html +++ b/commentaries/templates/commentaries/_commentary_summary.html @@ -46,5 +46,5 @@ {% if commentary.scipost_publication %} <br> - <p class="my-0">Published in {{commentary.scipost_publication.in_issue.in_volume.in_journal.get_name_display}}: <a href="{{commentary.scipost_publication.get_absolute_url}}">{{commentary.scipost_publication.in_issue.in_volume.in_journal.get_abbreviation_citation}} <strong>{{commentary.scipost_publication.in_issue.in_volume.number}}</strong>, {{commentary.scipost_publication.get_paper_nr}} ({{commentary.scipost_publication.publication_date|date:'Y'}})</a></p> + <p class="my-0">Published in {{commentary.scipost_publication.in_issue.in_volume.in_journal.get_name_display}}: <a href="{{commentary.scipost_publication.get_absolute_url}}">{{commentary.scipost_publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{commentary.scipost_publication.in_issue.in_volume.number}}</strong>, {{commentary.scipost_publication.get_paper_nr}} ({{commentary.scipost_publication.publication_date|date:'Y'}})</a></p> {% endif %} diff --git a/commentaries/templates/commentaries/commentary_list.html b/commentaries/templates/commentaries/commentary_list.html index 37a6123d03b8432b7871102fcc4f4ba4f70eaa91..c5fb016a6fe1fba8005fddeedf2935978c935e7e 100644 --- a/commentaries/templates/commentaries/commentary_list.html +++ b/commentaries/templates/commentaries/commentary_list.html @@ -26,7 +26,7 @@ <h2 class="card-title">Search SciPost Commentaries:</h2> <form action="{% url 'commentaries:commentaries' %}" class="small" method="get"> {{ form|bootstrap:'4,8,sm' }} - <input class="btn btn-sm btn-secondary" type="submit" value="Search"/> + <input class="btn btn-outline-secondary" type="submit" value="Search"/> </form> </div> </div> diff --git a/commentaries/templates/commentaries/modify_commentary_request.html b/commentaries/templates/commentaries/modify_commentary_request.html index c443012174802059f7e2b35f492ad33aa93dc22c..e364d6e617aa7fdc03b32fb7c84e67db395cdeb1 100644 --- a/commentaries/templates/commentaries/modify_commentary_request.html +++ b/commentaries/templates/commentaries/modify_commentary_request.html @@ -36,7 +36,7 @@ <form action="{% url 'commentaries:modify_commentary_request' commentary_id=commentary.id %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Submit and accept" /> + <input type="submit" class="btn btn-outline-secondary" value="Submit and accept" /> </form> </div> </div> diff --git a/commentaries/templates/commentaries/request_arxiv_preprint.html b/commentaries/templates/commentaries/request_arxiv_preprint.html index 5039bdb9845a13d07aca5503dba6f7a5708a7e19..2aef0a6af7905c21f74f206cedb3c5fade9a2737 100644 --- a/commentaries/templates/commentaries/request_arxiv_preprint.html +++ b/commentaries/templates/commentaries/request_arxiv_preprint.html @@ -22,7 +22,7 @@ <form action="{% url 'commentaries:prefill_using_arxiv_identifier' %}" method="post"> {% csrf_token %} {{ query_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Query arXiv"/> + <input class="btn btn-outline-secondary" type="submit" value="Query arXiv"/> </form> </div> </div> diff --git a/commentaries/templates/commentaries/request_published_article.html b/commentaries/templates/commentaries/request_published_article.html index 5ae3be2a1aac95aa3b3e30d59be8f242784b348b..d630c9662faf7491da22ad36062f670482f15953 100644 --- a/commentaries/templates/commentaries/request_published_article.html +++ b/commentaries/templates/commentaries/request_published_article.html @@ -23,7 +23,7 @@ <form action="{% url 'commentaries:prefill_using_DOI' %}" method="post"> {% csrf_token %} {{ query_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Query DOI"/> + <input class="btn btn-outline-secondary" type="submit" value="Query DOI"/> </form> </div> </div> diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py index 35e65abb64b3b50ed5f3aca96a9a65dbefce65a5..a24e9785c060b0da78756622c57ccec9e15d02f5 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -5,8 +5,8 @@ from django.test import TestCase from common.helpers import model_form_data from scipost.factories import UserFactory, ContributorFactory -from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory,\ - UnvettedArxivPreprintCommentaryFactory +from .factories import CommentaryFactory, UnvettedCommentaryFactory,\ + UnvettedUnpublishedCommentaryFactory from .forms import RequestPublishedArticleForm, VetCommentaryForm, DOIToQueryForm,\ ArxivQueryForm, RequestArxivPreprintForm from .models import Commentary @@ -189,7 +189,7 @@ class TestRequestArxivPreprintForm(TestCase): def setUp(self): add_groups_and_permissions() ContributorFactory.create_batch(5) - factory_instance = UnvettedArxivPreprintCommentaryFactory.build() + factory_instance = UnvettedUnpublishedCommentaryFactory.build() self.user = UserFactory() self.valid_form_data = model_form_data(factory_instance, RequestPublishedArticleForm) self.valid_form_data['arxiv_identifier'] = factory_instance.arxiv_identifier @@ -199,7 +199,7 @@ class TestRequestArxivPreprintForm(TestCase): self.assertTrue(form.is_valid()) def test_identifier_that_already_has_commentary_page_is_invalid(self): - commentary = UnvettedArxivPreprintCommentaryFactory() + commentary = UnvettedUnpublishedCommentaryFactory() invalid_data = {**self.valid_form_data, **{'arxiv_identifier': commentary.arxiv_identifier}} form = RequestArxivPreprintForm(invalid_data) self.assertEqual(form.is_valid(), False) diff --git a/commentaries/test_views.py b/commentaries/test_views.py index 24f46518623509e53016c7939f564ceee26d53cd..bddad132390fe8ecc387899c3fa7c46f7da2621c 100644 --- a/commentaries/test_views.py +++ b/commentaries/test_views.py @@ -5,8 +5,8 @@ from django.test import TestCase, Client, RequestFactory from scipost.models import Contributor from scipost.factories import ContributorFactory, UserFactory -from .factories import UnvettedCommentaryFactory, VettedCommentaryFactory, UnpublishedVettedCommentaryFactory, \ - UnvettedArxivPreprintCommentaryFactory +from .factories import UnvettedCommentaryFactory, CommentaryFactory, UnpublishedCommentaryFactory, \ + UnvettedUnpublishedCommentaryFactory from .forms import CommentarySearchForm, RequestPublishedArticleForm from .models import Commentary from .views import RequestPublishedArticle, prefill_using_DOI, RequestArxivPreprint @@ -57,7 +57,7 @@ class RequestArxivPreprintTest(TestCase): add_groups_and_permissions() self.target = reverse('commentaries:request_arxiv_preprint') self.contributor = ContributorFactory() - self.commentary_instance = UnvettedArxivPreprintCommentaryFactory.build(requested_by=self.contributor) + self.commentary_instance = UnvettedUnpublishedCommentaryFactory.build(requested_by=self.contributor) self.valid_form_data = model_form_data(self.commentary_instance, RequestPublishedArticleForm) # The form field is called 'identifier', while the model field is called 'arxiv_identifier', # so model_form_data doesn't include it. @@ -76,6 +76,7 @@ class RequestArxivPreprintTest(TestCase): self.assertEqual(commentary.arxiv_or_DOI_string, "arXiv:" + self.commentary_instance.arxiv_identifier) self.assertEqual(commentary.requested_by, self.contributor) + class VetCommentaryRequestsTest(TestCase): """Test cases for `vet_commentary_requests` view method""" @@ -119,7 +120,7 @@ class VetCommentaryRequestsTest(TestCase): # Only vetted Commentaries exist! # ContributorFactory.create_batch(5) - VettedCommentaryFactory(requested_by=ContributorFactory(), vetted_by=ContributorFactory()) + CommentaryFactory(requested_by=ContributorFactory(), vetted_by=ContributorFactory()) response = self.client.get(self.view_url) self.assertEquals(response.context['commentary_to_vet'], None) @@ -134,7 +135,7 @@ class BrowseCommentariesTest(TestCase): def setUp(self): add_groups_and_permissions() - VettedCommentaryFactory(discipline='physics', requested_by=ContributorFactory()) + CommentaryFactory(discipline='physics', requested_by=ContributorFactory()) self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', 'nrweeksback': '1' @@ -155,7 +156,7 @@ class CommentaryDetailTest(TestCase): def setUp(self): add_groups_and_permissions() self.client = Client() - self.commentary = UnpublishedVettedCommentaryFactory( + self.commentary = UnpublishedCommentaryFactory( requested_by=ContributorFactory(), vetted_by=ContributorFactory()) self.target = reverse( 'commentaries:commentary', diff --git a/commentaries/views.py b/commentaries/views.py index 015fb814d1a87c8739186b1c1001ecdad39df58c..532c8b2cfc14fbe56868959ca4c8f61a7b22f7de 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -233,13 +233,7 @@ def commentary_detail(request, arxiv_or_DOI_string): arxiv_or_DOI_string=arxiv_or_DOI_string) form = CommentForm() - try: - author_replies = Comment.objects.filter( - commentary=commentary, is_author_reply=True, status__gte=1) - except Comment.DoesNotExist: - author_replies = () - context = {'commentary': commentary, - 'author_replies': author_replies, 'form': form} + context = {'commentary': commentary, 'form': form} return render(request, 'commentaries/commentary_detail.html', context) diff --git a/comments/factories.py b/comments/factories.py index 1f76cf6a460001cb60077d1d5e73bdf075dd2c6a..bd6a3c784d52457cf1ae506921b27bb73176df37 100644 --- a/comments/factories.py +++ b/comments/factories.py @@ -1,29 +1,37 @@ +import random import factory -import pytz -from django.utils import timezone - -from commentaries.factories import VettedCommentaryFactory +from commentaries.models import Commentary from scipost.models import Contributor -from submissions.factories import EICassignedSubmissionFactory -from theses.factories import VettedThesisLinkFactory +from submissions.models import Submission, Report +from theses.models import ThesisLink from .constants import STATUS_VETTED from .models import Comment from faker import Faker -timezone.now() - class CommentFactory(factory.django.DjangoModelFactory): + status = STATUS_VETTED + vetted_by = factory.Iterator(Contributor.objects.all()) + author = factory.Iterator(Contributor.objects.all()) - comment_text = factory.lazy_attribute(lambda x: Faker().paragraph()) - remarks_for_editors = factory.lazy_attribute(lambda x: Faker().paragraph()) + comment_text = factory.Faker('paragraph') + remarks_for_editors = factory.Faker('paragraph') file_attachment = Faker().file_name(extension='pdf') - status = STATUS_VETTED # All comments will have status vetted! - vetted_by = factory.Iterator(Contributor.objects.all()) - date_submitted = Faker().date_time_between(start_date="-3y", end_date="now", tzinfo=pytz.UTC) + date_submitted = factory.Faker('date_this_decade') + + # Categories + is_cor = factory.Faker('boolean', chance_of_getting_true=20) + is_rem = factory.Faker('boolean', chance_of_getting_true=20) + is_que = factory.Faker('boolean', chance_of_getting_true=20) + is_ans = factory.Faker('boolean', chance_of_getting_true=20) + is_obj = factory.Faker('boolean', chance_of_getting_true=20) + is_rep = factory.Faker('boolean', chance_of_getting_true=20) + is_val = factory.Faker('boolean', chance_of_getting_true=20) + is_lit = factory.Faker('boolean', chance_of_getting_true=20) + is_sug = factory.Faker('boolean', chance_of_getting_true=20) class Meta: model = Comment @@ -31,16 +39,27 @@ class CommentFactory(factory.django.DjangoModelFactory): class CommentaryCommentFactory(CommentFactory): - content_object = factory.SubFactory(VettedCommentaryFactory) + content_object = factory.Iterator(Commentary.objects.all()) class SubmissionCommentFactory(CommentFactory): - content_object = factory.SubFactory(EICassignedSubmissionFactory) + content_object = factory.Iterator(Submission.objects.all()) + + @factory.post_generation + def replies(self, create, extracted, **kwargs): + if create: + for i in range(random.randint(0, 2)): + ReplyCommentFactory(content_object=self) + + +class ReplyCommentFactory(CommentFactory): + content_object = factory.SubFactory(SubmissionCommentFactory, replies=False) + is_author_reply = factory.Faker('boolean') class ThesislinkCommentFactory(CommentFactory): - content_object = factory.SubFactory(VettedThesisLinkFactory) + content_object = factory.Iterator(ThesisLink.objects.all()) -class ReplyCommentFactory(CommentFactory): - content_object = factory.SubFactory(SubmissionCommentFactory) +class ReportCommentFactory(CommentFactory): + content_object = factory.Iterator(Report.objects.all()) diff --git a/comments/migrations/0003_auto_20180314_1502.py b/comments/migrations/0003_auto_20180314_1502.py new file mode 100644 index 0000000000000000000000000000000000000000..585e4360923d274cd2c0204c18b1c5c9c4be9552 --- /dev/null +++ b/comments/migrations/0003_auto_20180314_1502.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 14:02 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0002_auto_20171229_1435'), + ] + + operations = [ + migrations.RemoveField( + model_name='comment', + name='commentary', + ), + migrations.RemoveField( + model_name='comment', + name='in_reply_to_comment', + ), + migrations.RemoveField( + model_name='comment', + name='in_reply_to_report', + ), + migrations.RemoveField( + model_name='comment', + name='submission', + ), + migrations.RemoveField( + model_name='comment', + name='thesislink', + ), + ] diff --git a/comments/models.py b/comments/models.py index e138ca39ad2b9a7fefc8d3f7606207bdfc176df5..960419d6cb51ad72ad873c0ba903523e4df52025 100644 --- a/comments/models.py +++ b/comments/models.py @@ -17,7 +17,8 @@ from .constants import COMMENT_STATUS, STATUS_PENDING from .managers import CommentQuerySet -WARNING_TEXT = 'Warning: Rather use/edit `content_object` instead or be 100% sure you know what you are doing!' +WARNING_TEXT = ('Warning: Rather use/edit `content_object` instead or be 100% sure you' + ' know what you are doing!') US_NOTICE = 'Warning: This field is out of service and will be removed in the future.' @@ -28,9 +29,9 @@ class Comment(TimeStampedModel): status = models.SmallIntegerField(default=STATUS_PENDING, choices=COMMENT_STATUS) vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, on_delete=models.CASCADE, related_name='comment_vetted_by') - file_attachment = models.FileField(upload_to='uploads/comments/%Y/%m/%d/', blank=True, - validators=[validate_file_extension, validate_max_file_size] - ) + file_attachment = models.FileField( + upload_to='uploads/comments/%Y/%m/%d/', blank=True, + validators=[validate_file_extension, validate_max_file_size]) # A Comment is always related to another model # This construction implicitly has property: `on_delete=models.CASCADE` @@ -40,23 +41,6 @@ class Comment(TimeStampedModel): nested_comments = GenericRelation('comments.Comment', related_query_name='comments') - # -- U/S - # These fields will be removed in the future. - # They still exists only to prevent possible data loss. - commentary = models.ForeignKey('commentaries.Commentary', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - submission = models.ForeignKey('submissions.Submission', blank=True, null=True, - on_delete=models.CASCADE, related_name='comments_old', - help_text=US_NOTICE) - thesislink = models.ForeignKey('theses.ThesisLink', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - in_reply_to_comment = models.ForeignKey('self', blank=True, null=True, - related_name="nested_comments_old", - on_delete=models.CASCADE, help_text=US_NOTICE) - in_reply_to_report = models.ForeignKey('submissions.Report', blank=True, null=True, - on_delete=models.CASCADE, help_text=US_NOTICE) - # -- End U/S - # Author info is_author_reply = models.BooleanField(default=False) author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, @@ -77,6 +61,7 @@ class Comment(TimeStampedModel): remarks_for_editors = models.TextField(blank=True, verbose_name='optional remarks for the Editors only') date_submitted = models.DateTimeField('date submitted', default=timezone.now) + # Opinions nr_A = models.PositiveIntegerField(default=0) in_agreement = models.ManyToManyField('scipost.Contributor', related_name='in_agreement', diff --git a/comments/templates/comments/_add_comment_form.html b/comments/templates/comments/_add_comment_form.html index 8225e51af3c7c597a496901cf3505b347e02678c..c420875cce1cc3553c0642e742af133fb320612d 100644 --- a/comments/templates/comments/_add_comment_form.html +++ b/comments/templates/comments/_add_comment_form.html @@ -21,11 +21,9 @@ {{form.comment_text|bootstrap:'12,12'}} <p> - In your comment, you can use LaTeX \$...\$ for in-text equations or \ [ ... \ ] for on-line equations. + In your comment, you can use $\LaTeX$ \$...\$ for in-text equations or \ [ ... \ ] for on-line equations. </p> - <p id="goodCommenter"><em> - Be professional. Only serious and meaningful comments will be vetted through. - </em></p> + <p class="good_commenter">Be professional. Only serious and meaningful comments will be vetted through.</p> </div> <div class="col-md-3 radio-list"> <label>Specify categorization(s):</label> diff --git a/comments/templates/comments/_comment_identifier.html b/comments/templates/comments/_comment_identifier.html index 5eb681610a2fd2e657a5d71fcea0a9d1656e16f3..9b7ac2da98169b92f313d3ddd9fa041ce0bc3d40 100644 --- a/comments/templates/comments/_comment_identifier.html +++ b/comments/templates/comments/_comment_identifier.html @@ -7,9 +7,8 @@ <div class="commentid" id="comment_id{{ comment.id }}"> <h3> {% if request.user.contributor and request.user.contributor == comment.core_content_object.editor_in_charge or is_edcol_admin and request.user|is_not_author_of_submission:comment.core_content_object.arxiv_identifier_w_vn_nr %} - <h3>{% if comment.anonymous %}(chose public anonymity) {% endif %}<a href="{{ comment.author.get_absolute_url }}">{{ comment.author.user.first_name }} {{ comment.author.user.last_name }}</a> + {% if comment.anonymous %}(chose public anonymity) {% endif %}<a href="{{ comment.author.get_absolute_url }}">{{ comment.author.user.first_name }} {{ comment.author.user.last_name }}</a> on {{ comment.date_submitted|date:'Y-m-d' }} - </h3> {% elif comment.anonymous %} Anonymous on {{comment.date_submitted|date:'Y-m-d'}} {% else %} @@ -17,7 +16,7 @@ <a href="{{comment.author.get_absolute_url}}">{{comment.author.user.first_name}} {{comment.author.user.last_name}}</a> on {{comment.date_submitted|date:'Y-m-d'}} {% endif %} - {% if comment.doi_string %} <small>{{ comment|citation }}</small>{% endif %} + {% if comment.doi_string %} <small>{{ comment|citation }}</small>{% endif %} </h3> diff --git a/comments/templates/comments/_single_comment.html b/comments/templates/comments/_single_comment.html index 665503b06852f4686845577dde1c1356898f831a..5c1137c2e4223106ef50268e61fc51e1a94b724d 100644 --- a/comments/templates/comments/_single_comment.html +++ b/comments/templates/comments/_single_comment.html @@ -15,24 +15,24 @@ <p class="my-3 pb-2"> {{ comment.comment_text|linebreaksbr }} - - {% if comment.file_attachment %} - <h3>Attachment:</h3> - <p> - <a target="_blank" href="{{ comment.get_attachment_url }}"> - {% if comment.file_attachment|is_image %} - <img class="attachment attachment-comment" src="{{ comment.get_attachment_url }}"> - {% else %} - {{ comment.file_attachment|filename }}<br><small>{{ comment.file_attachment.size|filesizeformat }}</small> - {% endif %} - </a> - </p> - {% endif %} </p> + {% if comment.file_attachment %} + <h3>Attachment:</h3> + <p> + <a target="_blank" href="{{ comment.get_attachment_url }}"> + {% if comment.file_attachment|is_image %} + <img class="attachment attachment-comment" src="{{ comment.get_attachment_url }}"> + {% else %} + {{ comment.file_attachment|filename }} + {% endif %} + </a> + </p> + {% endif %} + {% if is_editorial_college or is_edcol_admin %} {% if comment.remarks_for_editors %} <h3>Remarks for editors:</h3> - <p>{{ comment.remarks_for_editors|linebreaks }}</p> + <p>{{ comment.remarks_for_editors|linebreaksbr }}</p> {% endif %} {% endif %} diff --git a/comments/test_views.py b/comments/test_views.py index c16e256953ec7be3ea87ce46c9e96f16763cb15c..16d91d5d54adf61e629689bf455898b6acbc3448 100644 --- a/comments/test_views.py +++ b/comments/test_views.py @@ -6,7 +6,7 @@ from django.http import Http404 from scipost.factories import ContributorFactory from theses.factories import ThesisLinkFactory from submissions.factories import EICassignedSubmissionFactory -from commentaries.factories import UnpublishedVettedCommentaryFactory +from commentaries.factories import UnpublishedCommentaryFactory from .factories import CommentFactory from .forms import CommentForm @@ -84,7 +84,7 @@ class TestNewComment(TestCase): """ Valid Comment gets saved """ contributor = ContributorFactory() - commentary = UnpublishedVettedCommentaryFactory() + commentary = UnpublishedCommentaryFactory() valid_comment_data = model_form_data(CommentFactory, CommentForm) target = reverse('comments:new_comment', kwargs={'object_id': commentary.id, 'type_of_object': 'commentary'}) diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py index abe97df91f6222671cdcf4338f5ac1a39d0073ba..88f0fb9216d67f08929dc56e73245ed3da5fc8ee 100644 --- a/common/helpers/__init__.py +++ b/common/helpers/__init__.py @@ -32,8 +32,8 @@ def model_form_data(model, form_class, form_kwargs={}): return filter_keys(model_data, form_fields) -def random_arxiv_identifier_with_version_number(): - return random_arxiv_identifier_without_version_number() + "v0" +def random_arxiv_identifier_with_version_number(version_nr='0'): + return random_arxiv_identifier_without_version_number() + 'v' + str(version_nr) def random_arxiv_identifier_without_version_number(): @@ -44,15 +44,25 @@ def random_scipost_journal(): return random.choice(SCIPOST_JOURNALS_SUBMIT)[0] -def random_external_journal(): +def random_external_journal_abbrev(): return random.choice(( - 'PhysRevA.', - 'PhysRevB.', - 'PhysRevC.', - 'nature.' - 'S0550-3213(01)', - '1742-5468/', - '0550-3213(96)' + 'Ann. Phys.', + 'Phys. Rev. A', + 'Phys. Rev. B', + 'Phys. Rev. C', + 'Phys. Rev. Lett.', + 'Europhys. Lett.', + 'J. Math. Anal. Appl.', + 'Nat. Phys.' + 'J. Phys. A', + 'J. Stat. Phys.', + 'J. Stat. Mech.', + 'J. Math. Phys.', + 'Lett. Math. Phys.', + 'Sov. Phys. JETP', + 'Sov. Phys. JETP', + 'Nucl. Phys. B', + 'Adv. Phys.' )) @@ -64,14 +74,41 @@ def random_scipost_doi(): return '10.21468/%s.%s' % (random_scipost_journal(), random_pub_number()) +def random_scipost_report_doi_label(): + return 'SciPost.Report.%s' % random_digits(4) + + def random_external_doi(): - return '10.%s/%s%s' % (random_digits(5), random_external_journal(), random_pub_number()) + """ + Return a fake/random doi as if all journal abbrev and pub_number are separated by `.`, which + can be helpfull for testing purposes. + """ + journal = random.choice(( + 'PhysRevA', + 'PhysRevB', + 'PhysRevC', + 'PhysRevLett', + 'nature' + 'S0550-3213(01)', + '1742-5468', + '0550-3213(96)' + )) + return '10.%s/%s.%s' % (random_digits(5), journal, random_pub_number()) def random_digits(n): return "".join(random.choice(string.digits) for _ in range(n)) +def generate_orcid(): + return '{}-{}-{}-{}'.format( + random_digits(4), + random_digits(4), + random_digits(4), + random_digits(4), + ) + + def filter_keys(dictionary, keys_to_keep): # Field is empty if not on model. return {key: dictionary.get(key, "") for key in keys_to_keep} diff --git a/docs/users/_build/json/.buildinfo b/docs/users/_build/json/.buildinfo deleted file mode 100644 index d142458ff8b8295b327d0472a9bf472022ded4ae..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 4186a3ad9a6e6874e48739a86663d4c1 -tags: 6a38a8737c5234d37ffb85aa5f52e244 diff --git a/docs/users/_build/json/_sources/guides.txt b/docs/users/_build/json/_sources/guides.txt deleted file mode 100644 index 0d82f081b2cff1acacfb9cdea8584bf963b5e322..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides.txt +++ /dev/null @@ -1,10 +0,0 @@ -Guides -====== - - -.. toctree:: - :maxdepth: 2 - - guides/authors - guides/referees - guides/editorial diff --git a/docs/users/_build/json/_sources/guides/authors.txt b/docs/users/_build/json/_sources/guides/authors.txt deleted file mode 100644 index ee3d6110c650ec8107b14eaf630807ad85830802..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/authors.txt +++ /dev/null @@ -1,8 +0,0 @@ -Authoring -========= - -This guide is meant for **authors** of submissions and publications. - - -Preparing your manuscript -------------------------- diff --git a/docs/users/_build/json/_sources/guides/editorial.txt b/docs/users/_build/json/_sources/guides/editorial.txt deleted file mode 100644 index da3ee23fff6e4adbbfedf1915a0e21c71e49b05f..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial.txt +++ /dev/null @@ -1,10 +0,0 @@ -Editorial -========= - - -.. toctree:: - :maxdepth: 2 - - editorial/submissions_processing - editorial/production - editorial/maintenance diff --git a/docs/users/_build/json/_sources/guides/editorial/maintenance.txt b/docs/users/_build/json/_sources/guides/editorial/maintenance.txt deleted file mode 100644 index 02a6140f0dc143bba0638cacbad7c719a592c86a..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/maintenance.txt +++ /dev/null @@ -1,13 +0,0 @@ -Maintenance of SciPost Publications -=================================== - -This guide is meant for **Editorial Administrators**. -It describes the post-publication maintenance of the published material. - - -Maintenance ------------ - - -Cited-by updates -~~~~~~~~~~~~~~~~ diff --git a/docs/users/_build/json/_sources/guides/editorial/production.txt b/docs/users/_build/json/_sources/guides/editorial/production.txt deleted file mode 100644 index 752ff9c0e75ae59ff83e1abc94a584bb8069ac72..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/production.txt +++ /dev/null @@ -1,10 +0,0 @@ -Production -========== - - -.. toctree:: - :maxdepth: 2 - - production/initial_production - production/proofs - production/online_publication diff --git a/docs/users/_build/json/_sources/guides/editorial/production/initial_production.txt b/docs/users/_build/json/_sources/guides/editorial/production/initial_production.txt deleted file mode 100644 index d7448ae008b21eaced6f42827505a22fb5ab59b4..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/production/initial_production.txt +++ /dev/null @@ -1,427 +0,0 @@ -.. Howto for publication production - -.. _initial_production: - -Initial Production of SciPost Publications -========================================== - -This guide is meant for **Editorial Administrators**, **Production Supervisors** and **Production Officers**. It describes the post-acceptance workflow from paper acceptance to publication. - -Version: 2017-05-11. - - -Formatting check ----------------- - - If the Submission's references are not properly formatted, - and/or if these do not include the required DOIs, - the authors should be emailed and asked to provide them, - by sending in either an updated ``.bbl`` file or ``.bib`` source. - - Any extra material provided by the authors which supplements - the arXiv source should be put in a folder ``EXTRA_FROM_AUTH``. - - -Source retrieval and folder preparation ---------------------------------------- - - #. On the SciPost server, navigate to folder - ``[Journal full name]/IN_PRODUCTION``. - #. Create a folder - ``[journal abbrev. name]_[arxiv identifier]_[first author last name]``, - e.g. ``SciPost_Phys_1604.98141v3_Smart``. - #. Save the source from arXiv into this folder. - #. NOTE: the file will be named ``####.#####v#``, which is not recognized - as a ``.tar.gz`` file on a UNIX system. Rename the file - ``####.####v#.tar.gz``. Unzip it to produce the folder ``####.#####v#``. - If this produces another tar file, it is because the submission consists of - a single ``.tex`` file; you should then rename this to ``####.#####v#.tex``. - #. Copy the paper’s sources one level down (so from ``####.#####v#`` to - the current directory). BE CAREFUL: if the authors have included any of - the SciPost style files (``SciPost.cls``, ``SciPost_bibstyle.bst``), DO NOT - copy those down. You can skip this step if the previous step immediately led - to a ``.tex`` file. - #. Copy the files in - ``[Journal full name]/v##_production/FILES_TO_COPY_IN_PAPER_DIR`` - to the current directory. There are 5 files: - - * ``by.eps`` - * ``logo_scipost_with_bgd.pdf`` - * ``SciPost_bibstyle.bst`` - * ``SciPost.cls`` - * ``SciPost_[Phys, or other as appropriate]_Skeleton.tex`` - - #. Copy the skeleton ``.tex`` source to a new file using the name convention - ``[Journal abbrev. name]_####_#####v#_[first author last name].tex`` - (careful: use underscore instead of . between numbers). - - -LaTeX file preparation ----------------------- - - The next step is to transfer the submission's LaTeX contents into the final file. - - All steps involed appear in the skeleton ``.tex`` source in the form ``%%%%%%%%%% TODO: [TOKEN]`` opening marked, followed by a corresponding ``%%%%%%%%%% END TODO: [TOKEN]`` marker. - - The easiest way to proceed is to copy and paste material from the authors' ``.tex`` - source directly into the (appropriately renamed as per the instructions above) - skeleton file. - - During the file preparation, if there is anything worth noting about the - production process, please include this in the:: - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % Production Notes - % [your name here] - % - % [your notes here] - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - block at the beginning of the document, just before the ``\documentclass`` declaration. - - As the person running the production, please identify yourself by writing - your initials and surname in this block. - - -General LaTeX tips -~~~~~~~~~~~~~~~~~~ - - * Prefer the ``align`` (from package ``amsmath``) environment to ``eqnarray``. - For a technical discussion, see *e.g.* `this link <http://tug.org/TUGboat/tb33-1/tb103madsen.pdf>`_. - - You do **not** have to systematically replace all ``eqnarray`` with ``align``. - However, if you do reformat some equations, do shift to ``align``. - - -Step-by-step procedure -~~~~~~~~~~~~~~~~~~~~~~ - - #. TODO: PAPER CITATION - - In this place, fill the missing numbers in the citation header:: - - \rhead{\small \href{https://scipost.org/SciPostPhys.?.?.???}{SciPost Phys. ?, ??? (20??)}} - - - The first argument of the ``href`` is the simple permanent URL for the publication. This includes 3 numbers: the volume number, issue, and three-digit paper number, for example ``SciPostPhys.1.2.011``. Verify the appropriate issue number (this will be verified later by an EdAdmin). At this stage, leave the paper number to ``???``: this number will be assigned and filled in in the last stage of production. - - The second argument to the ``href`` uses the simple citation, dropping the issue number, for example ``SciPostPhys. 1, 011 (2016)``. - - - #. TODO: PACKAGES - - The ``SciPost.cls`` (v1b) class definition requires the following packages: - - * amsmath [NOTE: amssymb is redundant and clashes with mathdesign] - * cite - * doi - * fancyhdr - * geometry - * graphicx - * hyperref - * inputenc - * lineno [for proofs only] - * titlesec - * tocloft, nottoc,notlot,notlof - * xcolor - - If the authors are using extra packages not already in the list above, - you should paste their list within this TODO block. - - Any package originally included by the authors which you do **not** - include should be commented out with a ``% REMOVED IN PROD`` - comments prepended. - - Any package which is added during production should be listed after - the ``% ADDED IN PRODUCTION`` marker. - - - #. TODO: COMMANDS - - If the authors have redefined commands, paste the redefinitions in this block. - - Discard (namely: do not copy and paste) any length (and similar) redefinitions. - - - #. TODO: TITLE - - The title is enclosed in:: - - \begin{center}{\Large \textbf{\color{scipostdeepblue}{ - [title] - }}}\end{center} - - Paste the title in this block. End the title with ``\\`` - in order to ensure proper uniform spacing between the lines. - - - #. TODO: AUTHORS - - Author names should be in format ``firstname lastname``, e.g. ``John Smith``, - and should be in textbf. No ``comma`` but rather an ``and`` before - the last author. If multiple authors and affiliations, use ``\textsuperscript`` - and positive integer labels, with a ``$\star$`` for the corresponding author. - If multiple email addresses are given, reference them with ``$\dagger$, ...``. - - - #. TODO: AFFILIATIONS - - If there are many affiliations, each is prepended by the appropriate - ``{\bf [nr]}``. Separate affiliations with double-backslash. - - Put a ``\\[\baselineskip]`` after the affiliations. - - - #. TODO: EMAIL (OPTION) - - Optionally, provide the email of the corresponding author using:: - - ``\href{mailto:[email]}{\small \sf [email]}`` - - prepended with ``$\star$`` if corresponding author. If no email is to be given, - comment out the ``$\star$ \href{mailto:[email]}{\small \sf [email]}`` line. - - If a web link is offered, use the format ``{\small \url{[url]}}``. - - - #. TODO: ABSTRACT - - Paste the abstract in the provided block:: - - \section*{\color{scipostdeepblue}{Abstract}} - {\bf - [abstract] - } - - - #. TODO: TOC - - As a general guideline, the paper should contain a table of contents - if it has more than 6 pages. - - If a TOC should be included, leave the skeleton as it is. If no TOC - should be there, simply comment out the 2nd and 3rd lines of:: - - \vspace{10pt} - \noindent\rule{\textwidth}{1pt} - \tableofcontents - \noindent\rule{\textwidth}{1pt} - \vspace{10pt} - - explicitly leaving:: - - \vspace{10pt} - %\noindent\rule{\textwidth}{1pt} - %\tableofcontents - \noindent\rule{\textwidth}{1pt} - \vspace{10pt} - - If a single horizontal line is pushed to the next page, correct by - playing with negatime ``\vspace``. - - - #. TODO: COPYRIGHT - - Include the first author's initials and family name in the copyright - statement. If there are just two authors: give both authors' initials - and last names. If there are more than two authors, use the format - ``A. Bee {\it et al.}``. Be respectful of any special (non-latin) - characters in authors' names. - - - #. TODO: DATES - - Fill in the appropriate received and accepted dates in - format ``DD-MM-YYYY``. Be careful to use the correct submission data, - namely that of the original submission. - - The accepted and published dates will be filled in later on. - - - #. TODO: DOI - - Provide the volume and issue numbers (two places) in the DOI specifier. - The paper number will be assigned in the final stage of production. - - - #. TODO: LINENO - - During proofs stage, make sure line numbers are activated (they should - be by default). - - - #. TODO: CONTENTS - - Paste the entire bulk of the paper in this block, - including all sections and eventual appendices. - Check that there are no appendices after the references in the - original tex file. - - - #. TODO: BIBNR - - If the bibliography contains more than 100 entries, use - ``999`` instead of ``99`` in the ``\begin{thebibliotraphy}{[nr]}`` - statement. - - - #. TODO: BBL - - The references are explicitly pasted into this block. - - If using BiBTeX, use a ``\bibliography{[bibfilename]}`` command, - and comment out the ``\begin{thebibliography}`` and ``\end{thebibliography}`` - commands. After running BiBTeX, the contents of the generated - ``.bib`` file should be pasted in the uncommented ``\begin,\end{thebibliography}`` - block, and the ``\bibliography{[bibfilename]}`` should be commented out. - - *Note: the reason to not use BiBTeX from now on is to easy in-file - correction of improperly formatted references (instead of having to correct - the ``.bib`` file)*. - - - **You are now ready to typeset the ``.tex`` file**. Simple issues are listed - below. If you encounter further problems, see the **Problems** list below. - - If you need to run BiBTeX for the references, do so (remembering to do it - at least twice so the references appear), and then paste the contents of the - ``.bbl`` file in the ``% TODO: REFERENCES`` block. **Make sure you use the - correct** ``.bib`` **file**. - - - - -Simple issues -~~~~~~~~~~~~~ - - * *LaTeX Error: environment acknowledgements undefined* or - *Undefined control sequence \acknowledgements* - - The users have used ReVTeX; simply change the ``\begin{acknowledgements}`` - or ``\acknowledgements`` - to ``\section*{Acknowledgements}`` (of course also removing any eventual - ``\end{acknowledgements}``). - - - * *LaTeX Error: Environment widetext undefined.* - - The authors have used ReVTeX; simply comment out all ``\begin{widetext}`` - and ``\end{widetext}`` markers. - - -Problems -~~~~~~~~ - - * package ``lineno`` and ``amsmath`` are incompatible - - Problem: line numbers don't appear when paragraph is followed by align etc. - - Solution: [from `this link <http://phaseportrait.blogspot.nl/2007/08/lineno-and-amsmath-compatibility.html>`_]: paste this in the preamble:: - - %% Patch lineno when used with amsmath - \newcommand*\patchAmsMathEnvironmentForLineno[1]{% - \expandafter\let\csname old#1\expandafter\endcsname\csname #1\endcsname - \expandafter\let\csname oldend#1\expandafter\endcsname\csname end#1\endcsname - \renewenvironment{#1}% - {\linenomath\csname old#1\endcsname}% - {\csname oldend#1\endcsname\endlinenomath}}% - \newcommand*\patchBothAmsMathEnvironmentsForLineno[1]{% - \patchAmsMathEnvironmentForLineno{#1}% - \patchAmsMathEnvironmentForLineno{#1*}}% - \AtBeginDocument{% - \patchBothAmsMathEnvironmentsForLineno{equation}% - \patchBothAmsMathEnvironmentsForLineno{align}% - \patchBothAmsMathEnvironmentsForLineno{flalign}% - \patchBothAmsMathEnvironmentsForLineno{alignat}% - \patchBothAmsMathEnvironmentsForLineno{gather}% - \patchBothAmsMathEnvironmentsForLineno{multline}% - } - %% End patch lineno - - - * Breaking of in-line math equations - - Simply prevent by forcing equations into a math atom by surrouding them with braces,:: - - Here is an equation that should not be broken: ${E=mc^2}$. - - - * Equation/table or other text/maths element is just too wide. - Option: locally change the fontsize by embedding the object in a ``\fontsize`` block,:: - - \begingroup - \fontsize{new font size, e.g. 10pt}{skip, 120% of previous}\selectfont - [ element] - \endgroup - - - * package ``MnSymbol`` is problematic and clashes with amsmath. - - One solution is to import individual symbols according to these - `instructions <http://tex.stackexchange.com/questions/36006/importing-single-symbol-from-mnsymbol>`_. - - - * Equations spacing in ``align`` environment is too tight. - - The simple solution is to include a spacing specifier of the form ``\nonumber \\[5pt]``, - where ``5pt`` is a good compromise. - - This spacing can also be set globally by including ``\setlength{\jot}{5pt}`` before the - ``\begin{document}``. - - -References formatting -~~~~~~~~~~~~~~~~~~~~~ - - References should be in the following format: - - * Author names are in comma-separated list (except for the last author, - with no comma and an *and*) with format [initials] [last name]. - - * Titles are in italics, and capitalization is displayed (using *e.g.* \{\{ [title] \}\} in BiBTeX). For PRL: nouns capitalized. - - * Journal names are abbreviated. A useful resource is this `list of journal abbreviations <http://www.efm.leeds.ac.uk/~mark/ISIabbr/A_abrvjt.html>`_. - - * Volume number is in bold. - - * Issue number can be left out; if included, in parentheses after volume nr. - - * Year is in parentheses. - - * Commas separate all elements. - - * All doi are present and displayed in format doi:[doi]. Note that the doi does - *not* include any ``http://doi.org`` or similar URL prefix. Instead, it should - be of the form ``10.###[...]/[...]``. - - * The reference is closed by a ``.`` - - - For arXiv entries, verify if the paper has been published in the meantime. - If so, replace this reference with its proper citation. - If not, use the format ``\href{https://arxiv.org/abs/####.#####}{arXiv:####.#####}``, - and remove any ``(YEAR)``. - - \J. Stat. Mech. and JHEP are annoying (because the volume number is the year). - Manually remove volume nr for these, so the format becomes ``A. Bee, \emp{Bee's nice paper}, J. Stat. Mech.: Th. Exp. [P,L]##### (20##), \doi{10...}.`` - - \J. Phys. A is also annoying. Up to and including volume 39 (2006), it's - \J. Phys. A: Math. Gen. Afterwards, volume 40 (2007) onwards, it's - \J. Phys. A: Math. Theor. - - Entries in the bibliography which are not references but footnotes, - should be formatted as such in the main text (using ``\footnote{}``). - - - Check that all DOIs work. Remove the ``\meta`` at the end of the bibitem - if it is present. - - -Layout verification -~~~~~~~~~~~~~~~~~~~ - - The whole paper should be scanned through, and the layout of equations - and figures should be checked and corrected if necessary. - - In particular, the punctuation of equations should be checked and corrected - if necessary. diff --git a/docs/users/_build/json/_sources/guides/editorial/production/online_publication.txt b/docs/users/_build/json/_sources/guides/editorial/production/online_publication.txt deleted file mode 100644 index b190db227756fa26f3e93fb0fd0f0885a7f1fcbc..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/production/online_publication.txt +++ /dev/null @@ -1,148 +0,0 @@ -.. Howto for publication production - -Online Publication -================== - -This guide is meant for **Editorial Administrators**. It describes the final publication of manuscripts, after final author proofs approval. - -Version: 2017-05-11. - - -Finalization of manuscript production -------------------------------------- - -The steps described here follow up on the :doc:`../production/initial_production` instructions used by production officers. - - -Preparation of final version of record -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - #. Copy the whole paper folder from folder [journal name]/IN\_PRODUCTION \\to [journal name]/Volume\_[volume nr]. - - #. Check online to see which paper number is next available. - - #. Rename this folder using the convention [journal name]\_[volume number]([issue number])\_[paper nr]. - - #. Within this folder, take the author-accepted version tex file and rename it using the convention [journal name abbrev]\_[volume nr]\_[issue nr]\_[paper nr].tex. - - #. In this tex source, replace the ??? with the 3-digit paper number (3 places: 2 in preamble, 1 in copyright statement). - - #. Ensure that the author names are in format Abe Bee, Cee Dee and Elle Fine. - - #. Insert the correct Received, Accepted and Published dates in copyright statement. - - #. Make sure linenumbers are deactivated. - - #. Does the table of contents (if present) look OK? (Beware of hanging closing - line pushed to top of second page). If needed, adjust the ``\vspace`` spacings - defined around the table of contents, and/or insert an additional ``vspace`` - with negative spacing before the abstract. - - #. If the author-accepted proofs version used BiBTeX, copy the contents of the bbl - file into the .tex file, so that BiBTeX is not needed anymore. - - #. Verify each reference: authors, title, journal reference, doi link. - Manually correct any incorrect references. - - #. Recompile the LaTeX, and CAREFULLY CHECK EVERYTHING. - - -Uploading to ``scipost.org`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - - #. From the Submissions Pool, click on the link to initiate the publication process. - - #. Fill in the initiate publication form (using the dates in format YYYY-MM-DD). Submit. You are now on the validate publication page. - - #. Check that the paper number is correct. If not, modify the final tex source to account for this (see previous subsection). - - #. Select who the first author is (if registered as a Contributor; if not, inform the EdAdmin, and choose another author who is registered). - - #. Select the final version's pdf file. - - #. Submit. The paper is now published online. - - -Metadata preparation and DOI registration with Crossref -------------------------------------------------------- - - These tasks must be performed by **Editorial Administrators**, - who have access to the Publication's editorial tools - by navigating to the Publication's page. - - -Author listing -~~~~~~~~~~~~~~ - - If not all authors appear in the list presented at the top of the EdAdmin tools, - these should be added by following the ``Add a missing author`` link. - - The search form can be used to find missing authors who might be - Registered Contributors. If found, a one-click process adds them. - - You can otherwise create an UnregisteredAuthor object instance and link - it to the publication, by simply filling in the first and last name fields - and clicking on ``Add``. - - -Preparation of the citations list -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Follow the ``Create/update citation list metadata`` link. - - In the text area, paste the entire list of bibitems from the paper's - final ``.tex`` file. Upon clicking on ``Submit``, all DOI entires - are extracted and formatted into XML metadata which is saved in the - database. - - Citations with no valid DOI (*e.g.* arXiv preprints, books, etc) - do not appear in the metadata. - - -Funding info -~~~~~~~~~~~~ - - Following the ``Create/update funding info metadata`` link leads to a - page where the funding statement of the Publication's ``.tex`` file - (found either as a separate subsection or in the Acknowledgements) - can be pasted. - - -Preparation of the metadata XML -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Following the ``Create/update metadata XML`` link auto-generates a - full XML text field containing all the provided information, - properly formatted for the upcoming submission to Crossref. - - Verify that the first author is indeed enclosed in a - ``<person_name sequence='first' contributor_role='author'>`` tag, - and that subsequent authors (enclosed in - ``<person_name sequence='additional' contributor_role='author'>`` tags) - appear in the order of the Publication's author list. - - Once the metadata is set, clicking on ``Accept the metadata`` - saves the metadata to the database and returns one to the Publication's - page. - - -Metadata testing and deposit -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - At this stage, the metadata is ready for uploading to Crossref. - This occurs via a POST query to either the test or live Crossref server. - - Clicking on ``Test metadata deposit`` immediately posts the metadata - to the test server, and takes you to a page where the server's - response headers are displayed. The server also sends a more detailed - response via email - (to ``admin@scipost.org``; if you do not have access to this mailbox, - ask SciPost Administration) with the success status. - - Similarly, the actual deposit is immediately performed upon clicking on the - ``Deposit the metadata to Crossref``. The response headers are displayed, - and a detailed email response is sent by Crossref to ``admin@scipost.org``. - - - **This completes the publication process.** diff --git a/docs/users/_build/json/_sources/guides/editorial/production/proofs.txt b/docs/users/_build/json/_sources/guides/editorial/production/proofs.txt deleted file mode 100644 index eb50aee6c46c9efee9501d5b8d840453564dabc4..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/production/proofs.txt +++ /dev/null @@ -1,26 +0,0 @@ -.. Howto for proofs - -Proofs -====== - -This guide is meant for **Production supervisors**. It describes the proofs cycle between production team and manuscript authors. - - -Proofs version production -------------------------- - - * Once the paper has been properly formatted, the ``.tex`` and ``.pdf`` files - should be copied into new files carrying the ``_proofs_v[nr]`` suffix, - for example ``SciPost_Phys_1699_9999v9_Bee_proofs_v1.tex``. - - * The ``.pdf`` proofs should be emailed to the authors for verification. - Authors should return either an annotated pdf or a list of corrections - by plain text email. - - * Any modifications should be implemented directly in the main ``.tex`` file. - - * If any further check by the authors are required, start this proofs - todo-list again, increasing the proofs version number. - - * Once the authors have approved the proofs, the paper can be put forward - to online publication. diff --git a/docs/users/_build/json/_sources/guides/editorial/submissions_processing.txt b/docs/users/_build/json/_sources/guides/editorial/submissions_processing.txt deleted file mode 100644 index e8a3dd7ab7d30d6c93dbb6b4766fe72d42059d70..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/editorial/submissions_processing.txt +++ /dev/null @@ -1,39 +0,0 @@ -Submission processing -===================== - -This document describes the workflow starting at the moment of submission, and leading up to the final decision by the Editorial College. Editorial Administrators should use these instructions to correctly shepherd submissions through the whole refereeing process. - - -Reception phase ---------------- - -Plagiarism check -~~~~~~~~~~~~~~~~ - -Upon reception of a submission, the first action is to perform the plagiarism check. This is performed using Crossref Similarity Check (hereunder CSC), which makes use of the iThenticate application. EdAdmin should navigate to the `crosscheck page <https://crosscheck.ithenticate.com/en_us/login>`_ and login using the credentials (ask SciPost Admin if you don't have any). - -The .pdf of the submission should be obtained from arXiv, renamed ``####_#####v#_[lastname].pdf``, and saved in the shared folder ``SURFDRIVE/SCIPOST_SUBMISSIONS/[journal_name]/YYYY_MM``. This file should then be uploaded to CSC, in a folder ``[Journal name]_submissions/YYYY-MM`` (this folder should be created if this is that month's first submission; if creating the folder, select the option ``Exclude bibliography``). To upload, click on ``Submit a document``. You will be prompted for the (first) author first and last names, together with the document title (namely: the title of the paper). Clicking ``upload`` starts the plagiarism check (status becomes: processing). - -After a few minutes, the plagiarism check document becomes available (under Report). This should be examined thoroughly, downloaded and saved to the SURFDRIVE folder above. A percentage above about 15% is cause for concern. - - - -Pre-screening -------------- - - -Assignments -~~~~~~~~~~~ - - -During refereeing ------------------ - - -Compliance with Crossref DOI requirements -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If the Submission's references are not properly formatted, -and/or if these do not include the required DOIs, -the authors should be asked to do so in their eventual -resubmission. diff --git a/docs/users/_build/json/_sources/guides/referees.txt b/docs/users/_build/json/_sources/guides/referees.txt deleted file mode 100644 index 3c2f4012678982d4b07ac555e51e6364b9363f50..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/guides/referees.txt +++ /dev/null @@ -1,19 +0,0 @@ -Refereeing -========== - -This guide is meant for **referees**. It describes how to do an optimal job when writing reports on SciPost submissions. - - -Good citizenship as a referee ------------------------------ - - * Promptly responding to a refereeing invitation. - - * Submitting your report by the deadline. - - * Writing a high-quality report - - - -The characteristics of a high-quality report --------------------------------------------- diff --git a/docs/users/_build/json/_sources/index.txt b/docs/users/_build/json/_sources/index.txt deleted file mode 100644 index 3053a37cb65c6e0ddb0bbc501def50f7f58a0f90..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/index.txt +++ /dev/null @@ -1,23 +0,0 @@ -.. SciPost documentation master file, created by - sphinx-quickstart on Fri Nov 25 11:01:13 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to SciPost's documentation -================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - roles - guides - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/users/_build/json/_sources/roles.txt b/docs/users/_build/json/_sources/roles.txt deleted file mode 100644 index 9ee3d9b776d5a3c6962ce9b45958f8c90459ac60..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles.txt +++ /dev/null @@ -1,9 +0,0 @@ -Roles: responsibilities and task lists -====================================== - - -.. toctree:: - :maxdepth: 3 - - roles/editorial - roles/production diff --git a/docs/users/_build/json/_sources/roles/editorial.txt b/docs/users/_build/json/_sources/roles/editorial.txt deleted file mode 100644 index cfa655219fd7ad2e6fbb5c071998ccf46e409de5..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/editorial.txt +++ /dev/null @@ -1,9 +0,0 @@ -Editorial roles -=============== - - -.. toctree:: - :maxdepth: 3 - - editorial/editorial_administrators - editorial/editorial_fellows diff --git a/docs/users/_build/json/_sources/roles/editorial/editorial_administrators.txt b/docs/users/_build/json/_sources/roles/editorial/editorial_administrators.txt deleted file mode 100644 index d6747f7d357c5799fb77d8450dac71a258523b3d..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/editorial/editorial_administrators.txt +++ /dev/null @@ -1,6 +0,0 @@ -Editorial Administrators -======================== - - -.. toctree:: - :maxdepth: 2 diff --git a/docs/users/_build/json/_sources/roles/editorial/editorial_fellows.txt b/docs/users/_build/json/_sources/roles/editorial/editorial_fellows.txt deleted file mode 100644 index 00973f28a2fff53aa8726ed59a63da0bc3fd0122..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/editorial/editorial_fellows.txt +++ /dev/null @@ -1,6 +0,0 @@ -Editorial Fellows -================= - - -.. toctree:: - :maxdepth: 2 diff --git a/docs/users/_build/json/_sources/roles/production.txt b/docs/users/_build/json/_sources/roles/production.txt deleted file mode 100644 index e935976df5a41428183f2d63a8f12fe11817db4c..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/production.txt +++ /dev/null @@ -1,9 +0,0 @@ -Production roles -================ - - -.. toctree:: - :maxdepth: 1 - - production/production_supervisors - production/production_officers diff --git a/docs/users/_build/json/_sources/roles/production/production_officers.txt b/docs/users/_build/json/_sources/roles/production/production_officers.txt deleted file mode 100644 index 95a69c37640d688cbbd36c6bc6a840878677eb31..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/production/production_officers.txt +++ /dev/null @@ -1,12 +0,0 @@ -Production Officers -=================== - - -This document describes the roles of **Production Officers**. - - - -Tasks ------ - - * Upon being assigned a production stream, produce the first version of the proofs following the :ref:`initial production instructions <initial_production>`. diff --git a/docs/users/_build/json/_sources/roles/production/production_supervisors.txt b/docs/users/_build/json/_sources/roles/production/production_supervisors.txt deleted file mode 100644 index 8fb467e09935d058a266f1ea03efba6c763ce25e..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_sources/roles/production/production_supervisors.txt +++ /dev/null @@ -1,22 +0,0 @@ -Production Supervisors -====================== - - -This document describes the roles of **Production Supervisors**. - - -Responsibilities ----------------- - - * Ensuring the efficiency of the production workflow - * Supervising the work of the production team - * Training new Production Officers - - -Tasks ------ - - * Assign new production streams to Production Officers - * Check proofs produced by Production Officers - * Monitor the workflow of Production Officers - * Report back to EdAdmin on any problems with the workflow diff --git a/docs/users/_build/json/_static/pygments.css b/docs/users/_build/json/_static/pygments.css deleted file mode 100644 index 20c4814dcf0d3f437ee9a46f5957e3165aa5fb17..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/_static/pygments.css +++ /dev/null @@ -1,69 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #eeffcc; } -.highlight .c { color: #408090; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #007020; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #007020 } /* Comment.Preproc */ -.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #FF0000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #333333 } /* Generic.Output */ -.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #007020 } /* Keyword.Pseudo */ -.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #902000 } /* Keyword.Type */ -.highlight .m { color: #208050 } /* Literal.Number */ -.highlight .s { color: #4070a0 } /* Literal.String */ -.highlight .na { color: #4070a0 } /* Name.Attribute */ -.highlight .nb { color: #007020 } /* Name.Builtin */ -.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ -.highlight .no { color: #60add5 } /* Name.Constant */ -.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ -.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #007020 } /* Name.Exception */ -.highlight .nf { color: #06287e } /* Name.Function */ -.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ -.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #bb60d5 } /* Name.Variable */ -.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #208050 } /* Literal.Number.Bin */ -.highlight .mf { color: #208050 } /* Literal.Number.Float */ -.highlight .mh { color: #208050 } /* Literal.Number.Hex */ -.highlight .mi { color: #208050 } /* Literal.Number.Integer */ -.highlight .mo { color: #208050 } /* Literal.Number.Oct */ -.highlight .sa { color: #4070a0 } /* Literal.String.Affix */ -.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ -.highlight .sc { color: #4070a0 } /* Literal.String.Char */ -.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ -.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ -.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ -.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ -.highlight .sx { color: #c65d09 } /* Literal.String.Other */ -.highlight .sr { color: #235388 } /* Literal.String.Regex */ -.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ -.highlight .ss { color: #517918 } /* Literal.String.Symbol */ -.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #06287e } /* Name.Function.Magic */ -.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ -.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ -.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ -.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ -.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/users/_build/json/objects.inv b/docs/users/_build/json/objects.inv deleted file mode 100644 index 2b3fd971b6cd667e1183e31ae1fae9c1fa098a14..0000000000000000000000000000000000000000 --- a/docs/users/_build/json/objects.inv +++ /dev/null @@ -1,8 +0,0 @@ -# Sphinx inventory version 2 -# Project: SciPost -# Version: 1.0 -# The remainder of this file is compressed using zlib. -xÚT±ŽÛ0Ýó:t -„®·ÝЊš¡c H´¨,¢Ü»þ}eYŽåØ5œN"ßã“i‘5Z²ßƒ~2òŠŽŸ Ψ€oýq¨;Òȉ£JŒ„øšÎL²¯Î¯sFÀs -ÈÖ£5…H³¢ºå|Ã…N4’l@+ÂÍOÀ÷Bä*8+:9pꮆ”ä,/Zït§úä¦ÑDpºÅ[åY -º<î°Tö-a…ùÿ|£pÖÅK;ñ÷^j©ð’°Ò}Ó<†®â½†;µ;žËÂÜ]bî?¹ï”ÂÛz³úºDÀù†ÃŽ+ô¸:+cJÀ!êeÓðe2üD£\ƒÜøÃ>2Äd× CÛþñX¦ñ}ì¹|ÈØ±¨÷Àjœ^Y!íŸã˜ˆÃ‹ÌËÄ;3ïObWúã <rËÒ•L¼BdJ«!Hþ†8ð _ßw©bi©{í]¤nb8xîØ^Méö<ËlØVhŒ{Ûé—ɥїÊëËã>Wî¤Y_æã”Ë«*R¸Ö“mþÌæ%ƒ›Nܵè»ýf…dæwžð£ôêuþ:,Îò;ÉJy¤ \ No newline at end of file diff --git a/docs/users/guides/editorial/production/initial_production.rst b/docs/users/guides/editorial/production/initial_production.rst index d7448ae008b21eaced6f42827505a22fb5ab59b4..db4e7ddaee2de96a0b748d88ca6069ac5750c8f7 100644 --- a/docs/users/guides/editorial/production/initial_production.rst +++ b/docs/users/guides/editorial/production/initial_production.rst @@ -161,6 +161,8 @@ Step-by-step procedure and should be in textbf. No ``comma`` but rather an ``and`` before the last author. If multiple authors and affiliations, use ``\textsuperscript`` and positive integer labels, with a ``$\star$`` for the corresponding author. + If all authors are from the same institution, there is no need for superscripts + (except to mark the corresponding author if desired). If multiple email addresses are given, reference them with ``$\dagger$, ...``. diff --git a/funders/forms.py b/funders/forms.py index b7db43fcb0ecb60b2ed0e9e8ebb0e3afacc2b6fb..a88322618dda52734dd9e9418a89706f45660061 100644 --- a/funders/forms.py +++ b/funders/forms.py @@ -2,6 +2,7 @@ from django import forms from .models import Funder, Grant +from scipost.forms import HttpRefererFormMixin from scipost.models import Contributor @@ -12,24 +13,24 @@ class FunderRegistrySearchForm(forms.Form): class FunderForm(forms.ModelForm): class Meta: model = Funder - fields = ['name', 'acronym', 'identifier',] + fields = ['name', 'acronym', 'identifier'] class FunderSelectForm(forms.Form): funder = forms.ModelChoiceField(queryset=Funder.objects.all()) -class GrantForm(forms.ModelForm): +class GrantForm(HttpRefererFormMixin, forms.ModelForm): class Meta: model = Grant fields = ['funder', 'number', 'recipient_name', 'recipient', 'further_details'] def __init__(self, *args, **kwargs): - super(GrantForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['recipient'] = forms.ModelChoiceField( - queryset=Contributor.objects.all().order_by('user__last_name'), + queryset=Contributor.objects.select_related('user').order_by('user__last_name'), required=False) class GrantSelectForm(forms.Form): - grant = forms.ModelChoiceField(queryset=Grant.objects.all()) + grant = forms.ModelChoiceField(queryset=Grant.objects.all().select_related('funder')) diff --git a/funders/templates/funders/funders.html b/funders/templates/funders/funders.html index e3a3d70d3a27733024f34670ef93f192efdf83b7..0ab70cf7f9e3769407ee16b5fbf14ef0765ac7d9 100644 --- a/funders/templates/funders/funders.html +++ b/funders/templates/funders/funders.html @@ -19,10 +19,10 @@ <div class="tab-nav-inner"> <!-- Nav tabs --> <ul class="nav btn-group personal-page-nav" role="tablist"> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#funders" class="nav-link active" data-toggle="tab">Funders</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#grants" class="nav-link" data-toggle="tab">Grants</a> </li> </ul> @@ -49,7 +49,7 @@ <form action="{% url 'funders:query_crossref_for_funder' %}" method="post"> {% csrf_token %} {{form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Search"> + <input class="btn btn-outline-secondary" type="submit" value="Search"> </form> <br/> <h2>Funders in the SciPost database</h2> @@ -96,7 +96,7 @@ <form action="{% url 'funders:add_grant' %}" method="post"> {% csrf_token %} {{grant_form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Add"> + <input class="btn btn-outline-secondary" type="submit" value="Add"> </form> <br/> <h2>Grants in the SciPost database</h2> diff --git a/funders/templates/funders/grant_form.html b/funders/templates/funders/grant_form.html new file mode 100644 index 0000000000000000000000000000000000000000..65f59883b8a8e2577796626395becace74bba7b2 --- /dev/null +++ b/funders/templates/funders/grant_form.html @@ -0,0 +1,25 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Create new Grant{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Create new Grant</h1> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"> + </form> + </div> +</div> + +{% endblock content %} diff --git a/funders/templates/funders/query_crossref_for_funder.html b/funders/templates/funders/query_crossref_for_funder.html index 35c1b9a7ee15dbe05d6c0dabdcad77b83ce9e30e..7387f76ae0055a13ab2e0109c31f78e7dca402c7 100644 --- a/funders/templates/funders/query_crossref_for_funder.html +++ b/funders/templates/funders/query_crossref_for_funder.html @@ -12,7 +12,7 @@ <form action="{% url 'funders:query_crossref_for_funder' %}" method="post"> {% csrf_token %} {{form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Search"> + <input class="btn btn-outline-secondary" type="submit" value="Search"> </form> {% if response_headers %} <p>{{ response_headers }}</p> @@ -31,7 +31,7 @@ <input name='name' style="width: 64%" value='{{ item.name }}'> <input name='acronym' style="width: 64%" placeholder='acronym (if known)'> <input name='identifier' style="width: 64%" value='{{ item.uri }}'> - <input class="btn btn-secondary" type="submit" value="Add this funder"> + <input class="btn btn-outline-secondary" type="submit" value="Add this funder"> </form> </li> {% endfor %} @@ -39,7 +39,7 @@ <form action="{% url 'funders:add_funder' %}" method="post"> {% csrf_token %} {{funder_form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Submit"> + <input class="btn btn-outline-secondary" type="submit" value="Submit"> </form> {% endif %} </div> diff --git a/funders/urls.py b/funders/urls.py index 6766a095b6aead907943bc6f501317e76d1ce5ff..c885eeeecfdb062270f1273d507e0607b24d000e 100644 --- a/funders/urls.py +++ b/funders/urls.py @@ -9,5 +9,5 @@ urlpatterns = [ url(r'^add$', views.add_funder, name='add_funder'), url(r'^(?P<funder_id>[0-9]+)/$', views.funder_publications, name='funder_publications'), - url(r'^grants/add$', views.add_grant, name='add_grant'), + url(r'^grants/add$', views.CreateGrantView.as_view(), name='add_grant'), ] diff --git a/funders/views.py b/funders/views.py index b11b7a15baf0991b61bb864575f8d9e4a34a510f..c909254843a9b6fc1b5a06ee8d0874b73c6b1015 100644 --- a/funders/views.py +++ b/funders/views.py @@ -3,19 +3,24 @@ import json from django.contrib import messages from django.contrib.auth.decorators import permission_required -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy +from django.db import transaction +from django.utils.decorators import method_decorator +from django.views.generic.edit import CreateView from django.shortcuts import get_object_or_404, render, redirect from .models import Funder, Grant from .forms import FunderRegistrySearchForm, FunderForm, GrantForm +from scipost.mixins import PermissionsMixin + @permission_required('scipost.can_view_all_funding_info', raise_exception=True) def funders(request): funders = Funder.objects.all() form = FunderRegistrySearchForm() grants = Grant.objects.all() - grant_form = GrantForm() + grant_form = GrantForm(request=request) context = {'form': form, 'funders': funders, 'grants': grants, 'grant_form': grant_form} return render(request, 'funders/funders.html', context) @@ -53,7 +58,6 @@ def add_funder(request): return redirect(reverse('funders:funders')) -# @permission_required('scipost.can_view_all_funding_info', raise_exception=True) def funder_publications(request, funder_id): """ See details of specific Funder (publicly accessible). @@ -63,13 +67,24 @@ def funder_publications(request, funder_id): return render(request, 'funders/funder_details.html', context) -@permission_required('scipost.can_view_all_funding_info', raise_exception=True) -def add_grant(request): - grant_form = GrantForm(request.POST or None) - if grant_form.is_valid(): - grant = grant_form.save() - messages.success(request, ('<h3>Grant %s successfully added</h3>') % - str(grant)) - elif grant_form.has_changed(): - messages.warning(request, 'The form was invalidly filled (grant already exists?).') - return redirect(reverse('funders:funders')) +class HttpRefererMixin: + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs + + def form_valid(self, form): + if form.cleaned_data.get('http_referer'): + self.success_url = form.cleaned_data['http_referer'] + return super().form_valid(form) + + +@method_decorator(transaction.atomic, name='dispatch') +class CreateGrantView(PermissionsMixin, HttpRefererMixin, CreateView): + """ + Create a Grant in a separate window which may also be used by Production Supervisors. + """ + permission_required = 'scipost.can_create_grants' + model = Grant + form_class = GrantForm + success_url = reverse_lazy('funders:funders') diff --git a/invitations/mixins.py b/invitations/mixins.py index bf6629acd43fdf91ca9bb2d7d7e1fb4f6d59e9e4..fbb722b95b33dde713ecd2c21197b3ee5d095e54 100644 --- a/invitations/mixins.py +++ b/invitations/mixins.py @@ -1,6 +1,5 @@ from django.db import transaction from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from .constants import INVITATION_EDITORIAL_FELLOW from .models import RegistrationInvitation @@ -16,10 +15,6 @@ class RequestArgumentMixin: return kwargs -class PermissionsMixin(LoginRequiredMixin, PermissionRequiredMixin): - pass - - class BaseFormViewMixin: send_mail = None diff --git a/invitations/templates/invitations/registrationinvitation_form.html b/invitations/templates/invitations/registrationinvitation_form.html index 878ad6087d05b52a539f6ff611d3958c1d5b61b5..f6a7fb35b49ae79d8e4007e3f17138d0f579b0c7 100644 --- a/invitations/templates/invitations/registrationinvitation_form.html +++ b/invitations/templates/invitations/registrationinvitation_form.html @@ -23,7 +23,7 @@ <button type="submit" class="btn btn-primary" name="save" value="save">Save</button> <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_create">Save and create new</button> {% if perms.scipost.can_manage_registration_invitations %} - <button type="submit" class="ml-2 btn btn-secondary" name="save" value="save_and_send">Save and send mail</button> + <button type="submit" class="ml-2 btn btn-outline-secondary" name="save" value="save_and_send">Save and send mail</button> {% endif %} </form> </div> diff --git a/invitations/templates/invitations/registrationinvitation_form_add_new.html b/invitations/templates/invitations/registrationinvitation_form_add_new.html index ce28c28957ac3343c41616010ba93e372da82bb7..5e011e4f28956358c018b636ac5b9c9c4a2dbfd4 100644 --- a/invitations/templates/invitations/registrationinvitation_form_add_new.html +++ b/invitations/templates/invitations/registrationinvitation_form_add_new.html @@ -72,7 +72,7 @@ <button type="submit" class="btn btn-primary" name="save" value="save">Save</button> <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_create">Save and create new</button> {% if perms.scipost.can_manage_registration_invitations %} - <button type="submit" class="ml-2 btn btn-secondary" name="save" value="save_and_send">Save and send mail</button> + <button type="submit" class="ml-2 btn btn-outline-secondary" name="save" value="save_and_send">Save and send mail</button> {% endif %} </form> diff --git a/invitations/views.py b/invitations/views.py index 6967619d1a03a73963b67a2916df0dbffb0bf685..fdad1c7896970f468b2236f0e3eddc0ef941cb44 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -10,12 +10,12 @@ from .forms import RegistrationInvitationForm, RegistrationInvitationReminderFor RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\ CitationNotificationForm, SuggestionSearchForm, RegistrationInvitationFilterForm,\ CitationNotificationProcessForm, RegistrationInvitationAddCitationForm -from .mixins import RequestArgumentMixin, PermissionsMixin, SaveAndSendFormMixin, SendMailFormMixin +from .mixins import RequestArgumentMixin, SaveAndSendFormMixin, SendMailFormMixin from .models import RegistrationInvitation, CitationNotification from scipost.models import Contributor -from scipost.mixins import PaginationMixin -from mails.mixins import MailEditorMixin +from scipost.mixins import PaginationMixin, PermissionsMixin +from mails.views import MailEditorMixin class RegistrationInvitationsView(PaginationMixin, PermissionsMixin, ListView): diff --git a/journals/admin.py b/journals/admin.py index 8fccd05cca1475fe757dfacc81748b78ab7fa9f2..956df25fea384d0aa0c737c6ab49740e2a2779f2 100644 --- a/journals/admin.py +++ b/journals/admin.py @@ -55,6 +55,7 @@ class PublicationAdminForm(forms.ModelForm): class ReferenceInline(admin.TabularInline): model = Reference + extra = 0 class AuthorsInline(admin.TabularInline): @@ -64,7 +65,7 @@ class AuthorsInline(admin.TabularInline): class PublicationAdmin(admin.ModelAdmin): search_fields = ['title', 'author_list'] - list_display = ['title', 'author_list', 'in_issue', 'doi_string', 'publication_date'] + list_display = ['title', 'author_list', 'in_issue', 'doi_string', 'publication_date', 'status'] date_hierarchy = 'publication_date' list_filter = ['in_issue'] inlines = [AuthorsInline, ReferenceInline] diff --git a/journals/behaviors.py b/journals/behaviors.py index eca874354c8a6458dd3fa6804c38f7770c65e2a7..8079e19c758b972d28c1dafc4869fd62f90f9cdf 100644 --- a/journals/behaviors.py +++ b/journals/behaviors.py @@ -1,5 +1,8 @@ from django.core.validators import RegexValidator +from .constants import PUBLICATION_DOI_VALIDATION_REGEX + + doi_journal_validator = RegexValidator(r'^[a-zA-Z]+$', 'Only valid DOI expressions are allowed ([a-zA-Z]+).') doi_volume_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+$', @@ -7,6 +10,7 @@ doi_volume_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+$', doi_issue_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+.[0-9]+$', ('Only valid DOI expressions are allowed ' '([a-zA-Z]+.[0-9]+.[0-9]+).')) -doi_publication_validator = RegexValidator(r'^[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}$', - ('Only valid DOI expressions are allowed ' - '([a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}).')) +doi_publication_validator = RegexValidator( + r'^{regex}$'.format(regex=PUBLICATION_DOI_VALIDATION_REGEX), + ('Only valid DOI expressions are allowed ' + '(`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')) diff --git a/journals/constants.py b/journals/constants.py index da5c678eae99d18964113ca71b204208c397769e..60a5440145aa2ac1acbb33326f38151c3b398dc0 100644 --- a/journals/constants.py +++ b/journals/constants.py @@ -26,6 +26,7 @@ REGEX_CHOICES = '|'.join([ SCIPOST_JOURNAL_PHYSICS ]) +PUBLICATION_DOI_REGEX = PUBLICATION_DOI_VALIDATION_REGEX = '[a-zA-Z]+.[0-9]+(.[0-9]+.[0-9]{3,})?' SCIPOST_JOURNALS_DOMAINS = ( ('E', 'Experimental'), @@ -57,6 +58,13 @@ ISSUE_STATUSES = ( (STATUS_PUBLISHED, 'Published'), ) +PUBLICATION_PREPUBLISHED, PUBLICATION_PUBLISHED = ('prepub', 'pub') +PUBLICATION_STATUSES = ( + (STATUS_DRAFT, 'Draft'), + (PUBLICATION_PREPUBLISHED, 'Pre-published'), + (PUBLICATION_PUBLISHED, 'Published'), +) + CCBY4 = 'CC BY 4.0' CCBYSA4 = 'CC BY-SA 4.0' CCBYNC4 = 'CC BY-NC 4.0' @@ -71,3 +79,13 @@ CC_LICENSES_URI = ( (CCBYSA4, 'https://creativecommons.org/licenses/by-sa/4.0'), (CCBYNC4, 'https://creativecommons.org/licenses/by-nc/4.0'), ) + + +ISSUES_AND_VOLUMES = 'IV' +ISSUES_ONLY = 'IO' +INDIVIDUAL_PUBLCATIONS = 'IP' +JOURNAL_STRUCTURE = ( + (ISSUES_AND_VOLUMES, 'Issues and Volumes'), + # (ISSUES_ONLY, 'Issues only'), # This option complies with Crossref's rules, but is not implemented (yet). + (INDIVIDUAL_PUBLCATIONS, 'Individual Publications'), +) diff --git a/journals/factories.py b/journals/factories.py index 802dc70c73bd9ebd6ebe630e85adfb757de6a093..a49839f1e1b195f3e5822ba6f14ecd3a8b1553b6 100644 --- a/journals/factories.py +++ b/journals/factories.py @@ -1,18 +1,37 @@ import factory import datetime import pytz +import random -from django.utils import timezone - -from common.helpers import random_digits -from journals.constants import SCIPOST_JOURNALS +from common.helpers import random_digits, random_external_doi, random_external_journal_abbrev +from journals.constants import SCIPOST_JOURNALS, SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES,\ + ISSUES_AND_VOLUMES, INDIVIDUAL_PUBLCATIONS, PUBLICATION_PUBLISHED from submissions.factories import PublishedSubmissionFactory -from .models import Journal, Volume, Issue, Publication +from .models import Journal, Volume, Issue, Publication, Reference from faker import Faker +class ReferenceFactory(factory.django.DjangoModelFactory): + reference_number = factory.LazyAttribute(lambda o: o.publication.references.count() + 1) + identifier = factory.lazy_attribute(lambda n: random_external_doi()) + link = factory.Faker('uri') + + class Meta: + model = Reference + + @factory.lazy_attribute + def citation(self): + faker = Faker() + return '<em>{}</em> {} <b>{}</b>, {} ({})'.format( + faker.sentence(), + random_external_journal_abbrev(), + random.randint(1, 100), + random.randint(1, 100), + faker.year()) + + class JournalFactory(factory.django.DjangoModelFactory): name = factory.Iterator(SCIPOST_JOURNALS, getter=lambda c: c[0]) doi_label = factory.Iterator(SCIPOST_JOURNALS, getter=lambda c: c[0]) @@ -20,25 +39,21 @@ class JournalFactory(factory.django.DjangoModelFactory): class Meta: model = Journal - django_get_or_create = ('name', 'doi_label',) + django_get_or_create = ('name',) + + @factory.lazy_attribute + def structure(self): + if self.name == SCIPOST_JOURNAL_PHYSICS_LECTURE_NOTES: + return INDIVIDUAL_PUBLCATIONS + return ISSUES_AND_VOLUMES class VolumeFactory(factory.django.DjangoModelFactory): in_journal = factory.SubFactory(JournalFactory) - number = 9999 - doi_label = factory.Faker('md5') - - @factory.post_generation - def doi(self, create, extracted, **kwargs): - self.number = self.in_journal.volume_set.count() - self.doi_label = self.in_journal.doi_label + '.' + str(self.number) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - self.start_date = Faker().date_time_between(start_date="-3y", end_date="now", - tzinfo=pytz.UTC) - self.until_date = self.start_date + datetime.timedelta(weeks=26) + doi_label = factory.lazy_attribute(lambda o: '%s.%i' % (o.in_journal.doi_label, o.number)) + number = factory.lazy_attribute(lambda o: o.in_journal.volumes.count() + 1) + start_date = factory.Faker('date_time_this_decade') + until_date = factory.lazy_attribute(lambda o: o.start_date + datetime.timedelta(weeks=26)) class Meta: model = Volume @@ -47,21 +62,12 @@ class VolumeFactory(factory.django.DjangoModelFactory): class IssueFactory(factory.django.DjangoModelFactory): in_volume = factory.Iterator(Volume.objects.all()) - number = 9999 - doi_label = factory.Faker('md5') + number = factory.LazyAttribute(lambda o: o.in_volume.issues.count() + 1) + doi_label = factory.LazyAttribute(lambda o: '%s.%i' % (o.in_volume.doi_label, o.number)) - @factory.post_generation - def doi(self, create, extracted, **kwargs): - self.number = self.in_volume.issue_set.count() - self.doi_label = self.in_volume.doi_label + '.' + str(self.number) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - self.start_date = Faker().date_time_between(start_date=self.in_volume.start_date, - end_date=self.in_volume.until_date, - tzinfo=pytz.UTC) - self.until_date = self.start_date + datetime.timedelta(weeks=4) + start_date = factory.LazyAttribute(lambda o: Faker().date_time_between( + start_date=o.in_volume.start_date, end_date=o.in_volume.until_date, tzinfo=pytz.UTC)) + until_date = factory.LazyAttribute(lambda o: o.start_date + datetime.timedelta(weeks=4)) class Meta: model = Issue @@ -69,42 +75,99 @@ class IssueFactory(factory.django.DjangoModelFactory): class PublicationFactory(factory.django.DjangoModelFactory): - accepted_submission = factory.SubFactory(PublishedSubmissionFactory) + accepted_submission = factory.SubFactory( + PublishedSubmissionFactory, generate_publication=False) paper_nr = 9999 - pdf_file = Faker().file_name(extension='pdf') - in_issue = factory.Iterator(Issue.objects.all()) - submission_date = factory.Faker('date') - acceptance_date = factory.Faker('date') - publication_date = factory.Faker('date') - doi_label = factory.Faker('md5') + pdf_file = factory.Faker('file_name', extension='pdf') + status = PUBLICATION_PUBLISHED + submission_date = factory.Faker('date_this_year') + acceptance_date = factory.Faker('date_this_year') + publication_date = factory.Faker('date_this_year') + + discipline = factory.LazyAttribute(lambda o: o.accepted_submission.discipline) + domain = factory.LazyAttribute(lambda o: o.accepted_submission.domain) + subject_area = factory.LazyAttribute(lambda o: o.accepted_submission.subject_area) + title = factory.LazyAttribute(lambda o: o.accepted_submission.title) + abstract = factory.LazyAttribute(lambda o: o.accepted_submission.abstract) + + # Dates + submission_date = factory.LazyAttribute(lambda o: o.accepted_submission.submission_date) + acceptance_date = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + publication_date = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + latest_activity = factory.LazyAttribute(lambda o: o.accepted_submission.latest_activity) + + # Authors + author_list = factory.LazyAttribute(lambda o: o.accepted_submission.author_list) + + class Meta: + model = Publication + django_get_or_create = ('accepted_submission', ) + + class Params: + journal = None + + @factory.lazy_attribute + def in_issue(self): + # Make sure Issues, Journals and doi are correct. + if self.journal: + journal = Journal.objects.get(name=self.journal) + else: + journal = Journal.objects.order_by('?').first() + + if journal.has_issues: + return Issue.objects.for_journal(journal.name).order_by('?').first() + return None + + @factory.lazy_attribute + def in_journal(self): + # Make sure Issues, Journals and doi are correct. + if self.journal: + journal = Journal.objects.get(name=self.journal) + elif not self.in_issue: + journal = Journal.objects.has_individual_publications().order_by('?').first() + else: + return None + + if not journal.has_issues: + # Keep this logic in case self.journal is set. + return journal + return None + + @factory.lazy_attribute + def paper_nr(self): + if self.in_issue: + return self.in_issue.publications.count() + 1 + elif self.in_journal: + return self.in_journal.publications.count() + 1 + + @factory.lazy_attribute + def doi_label(self): + if self.in_issue: + return self.in_issue.doi_label + '.' + str(self.paper_nr).rjust(3, '0') + elif self.in_journal: + return '%s.%i' % (self.in_journal.doi_label, self.paper_nr) @factory.post_generation - def doi(self, create, extracted, **kwargs): - paper_nr = self.in_issue.publication_set.count() - self.paper_nr = paper_nr - self.doi_label = self.in_issue.doi_label + '.' + str(paper_nr).rjust(3, '0') + def generate_publication(self, create, extracted, **kwargs): + if create and extracted is not False: + return + + from journals.factories import PublicationFactory + factory.RelatedFactory( + PublicationFactory, 'accepted_submission', + title=self.title, author_list=self.author_list) @factory.post_generation - def submission_data(self, create, extracted, **kwargs): - # Content - self.discipline = self.accepted_submission.discipline - self.domain = self.accepted_submission.domain - self.subject_area = self.accepted_submission.subject_area - self.title = self.accepted_submission.title - self.abstract = self.accepted_submission.abstract - - # Authors - self.author_list = self.accepted_submission.author_list - # self.authors.add(*self.accepted_submission.authors.all()) - self.authors_claims.add(*self.accepted_submission.authors_claims.all()) - self.authors_false_claims.add(*self.accepted_submission.authors_false_claims.all()) + def author_relations(self, create, extracted, **kwargs): + if not create: + return - # Dates - self.submission_date = self.accepted_submission.latest_activity - self.acceptance_date = self.accepted_submission.latest_activity - self.publication_date = self.accepted_submission.latest_activity - self.latest_activity = self.accepted_submission.latest_activity + # Append references + for i in range(5): + ReferenceFactory(publication=self) - class Meta: - model = Publication - django_get_or_create = ('accepted_submission', ) + # Copy author data from Submission + for author in self.accepted_submission.authors.all(): + self.authors.create(publication=self, contributor=author) + self.authors_claims.add(*self.accepted_submission.authors_claims.all()) + self.authors_false_claims.add(*self.accepted_submission.authors_false_claims.all()) diff --git a/journals/forms.py b/journals/forms.py index 55d0cb2bcdf4c8435ba6be7605d2a634442845ce..0c6ea15bcd423d55e652191e2e2041357cafd240 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -1,102 +1,162 @@ +import hashlib +import os +import random import re +import string from datetime import datetime from django import forms +from django.conf import settings from django.forms import BaseModelFormSet, modelformset_factory +from django.template import loader from django.utils import timezone -from .models import Issue, Publication, Reference, UnregisteredAuthor +from .constants import STATUS_DRAFT, PUBLICATION_PREPUBLISHED, PUBLICATION_PUBLISHED +from .exceptions import PaperNumberingError +from .models import Issue, Publication, Reference, UnregisteredAuthor, PublicationAuthorsTable +from .utils import JournalUtils +from .signals import notify_manuscript_published + +from funders.models import Grant, Funder +from journals.models import Journal +from mails.utils import DirectMailUtil +from production.constants import PROOFS_PUBLISHED +from production.models import ProductionEvent +from production.signals import notify_stream_status_change +from scipost.forms import RequestFormMixin from scipost.services import DOICaller from submissions.models import Submission -class InitiatePublicationForm(forms.Form): - accepted_submission = forms.ModelChoiceField(queryset=Submission.objects.accepted()) - to_be_issued_in = forms.ModelChoiceField( - queryset=Issue.objects.filter(until_date__gte=timezone.now())) - - def __init__(self, *args, **kwargs): - super(InitiatePublicationForm, self).__init__(*args, **kwargs) - - -class ValidatePublicationForm(forms.ModelForm): - class Meta: - model = Publication - exclude = ['authors_claims', 'authors_false_claims', - 'metadata', 'metadata_xml', 'authors_registered', - 'authors_unregistered', 'latest_activity'] - - class UnregisteredAuthorForm(forms.ModelForm): class Meta: model = UnregisteredAuthor fields = ('first_name', 'last_name') -class CitationListBibitemsForm(forms.Form): +class CitationListBibitemsForm(forms.ModelForm): latex_bibitems = forms.CharField(widget=forms.Textarea()) + class Meta: + model = Publication + fields = () + def __init__(self, *args, **kwargs): - super(CitationListBibitemsForm, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.fields['latex_bibitems'].widget.attrs.update( - {'rows': 30, 'cols': 50, 'placeholder': 'Paste the .tex bibitems here'}) + {'placeholder': 'Paste the .tex bibitems here'}) def extract_dois(self): entries_list = self.cleaned_data['latex_bibitems'] entries_list = re.sub(r'(?m)^\%.*\n?', '', entries_list) entries_list = entries_list.split('\doi{') dois = [] - nentries = 1 + n_entry = 1 for entry in entries_list[1:]: # drop first bit before first \doi{ dois.append( - {'key': 'ref' + str(nentries), + {'key': 'ref' + str(n_entry), 'doi': entry.partition('}')[0], } ) - nentries += 1 + n_entry += 1 return dois + def save(self, *args, **kwargs): + self.instance.metadata['citation_list'] = self.extract_dois() + return super().save(*args, **kwargs) + class FundingInfoForm(forms.ModelForm): funding_statement = forms.CharField(widget=forms.Textarea({ - 'rows': 10, - 'placeholder': 'Paste the funding info statement here' - })) + 'placeholder': 'Paste the funding info statement here'})) class Meta: model = Publication fields = () + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['funding_statement'].initial = self.instance.metadata.get('funding_statement') + def save(self, *args, **kwargs): self.instance.metadata['funding_statement'] = self.cleaned_data['funding_statement'] return super().save(*args, **kwargs) +class BasePublicationAuthorsTableFormSet(BaseModelFormSet): + def save(self, *args, **kwargs): + objects = super().save(*args, **kwargs) + for form in self.ordered_forms: + form.instance.order = form.cleaned_data['ORDER'] + form.instance.save() + return objects + + +PublicationAuthorOrderingFormSet = modelformset_factory( + PublicationAuthorsTable, fields=(), can_order=True, extra=0, + formset=BasePublicationAuthorsTableFormSet) + + class CreateMetadataXMLForm(forms.ModelForm): class Meta: model = Publication fields = ['metadata_xml'] def __init__(self, *args, **kwargs): + kwargs['initial'] = { + 'metadata_xml': self.new_xml(kwargs.get('instance')) + } super().__init__(*args, **kwargs) - self.fields['metadata_xml'].widget.attrs.update({'rows': 50}) + + def save(self, *args, **kwargs): + self.instance.latest_metadata_update = timezone.now() + return super().save(*args, **kwargs) + + def new_xml(self, publication): + """ + Create new XML structure, return as a string + """ + # Create a doi_batch_id + salt = "" + for i in range(5): + salt = salt + random.choice(string.ascii_letters) + salt = salt.encode('utf8') + idsalt = publication.title[:10] + idsalt = idsalt.encode('utf8') + doi_batch_id = hashlib.sha1(salt+idsalt).hexdigest() + + funders = (Funder.objects.filter(grant__in=publication.grants.all()) + | publication.funders_generic.all()).distinct() + + # Render from template + template = loader.get_template('xml/publication_crossref.html') + context = { + 'publication': publication, + 'doi_batch_id': doi_batch_id, + 'deposit_email': settings.CROSSREF_DEPOSIT_EMAIL, + 'funders': funders, + } + return template.render(context) class CreateMetadataDOAJForm(forms.ModelForm): class Meta: model = Publication - fields = () + fields = ['metadata_DOAJ'] def __init__(self, *args, **kwargs): self.request = kwargs.pop('request') + kwargs['initial'] = { + 'metadata_DOAJ': self.generate(kwargs.get('instance')) + } super().__init__(*args, **kwargs) - def save(self, *args, **kwargs): - self.instance.metadata_DOAJ = self.generate(self.instance) - return super().save(*args, **kwargs) - def generate(self, publication): + if publication.in_issue: + issn = str(publication.in_issue.in_volume.in_journal.issn) + else: + issn = str(publication.in_journal.issn) md = { 'bibjson': { 'author': [{'name': publication.author_list}], @@ -108,7 +168,7 @@ class CreateMetadataDOAJForm(forms.ModelForm): 'identifier': [ { 'type': 'eissn', - 'id': str(publication.in_issue.in_volume.in_journal.issn) + 'id': issn }, { 'type': 'doi', @@ -121,28 +181,48 @@ class CreateMetadataDOAJForm(forms.ModelForm): 'type': 'fulltext', } ], - 'journal': { - 'publisher': 'SciPost', - 'volume': str(publication.in_issue.in_volume.number), - 'number': str(publication.in_issue.number), - 'identifier': [{ - 'type': 'eissn', - 'id': str(publication.in_issue.in_volume.in_journal.issn) - }], - 'license': [ - { - 'url': self.request.build_absolute_uri( - publication.in_issue.in_volume.in_journal.get_absolute_url()), - 'open_access': 'true', - 'type': publication.get_cc_license_display(), - 'title': publication.get_cc_license_display(), - } - ], - 'language': ['EN'], - 'title': publication.in_issue.in_volume.in_journal.get_name_display(), - } } } + if publication.in_issue: + md['journal'] = { + 'publisher': 'SciPost', + 'volume': str(publication.in_issue.in_volume.number), + 'number': str(publication.in_issue.number), + 'identifier': [{ + 'type': 'eissn', + 'id': issn + }], + 'license': [ + { + 'url': self.request.build_absolute_uri( + publication.in_issue.in_volume.in_journal.get_absolute_url()), + 'open_access': 'true', + 'type': publication.get_cc_license_display(), + 'title': publication.get_cc_license_display(), + } + ], + 'language': ['EN'], + 'title': publication.in_issue.in_volume.in_journal.get_name_display(), + } + else: + md['journal'] = { + 'publisher': 'SciPost', + 'identifier': [{ + 'type': 'eissn', + 'id': issn + }], + 'license': [ + { + 'url': self.request.build_absolute_uri( + publication.in_journal.get_absolute_url()), + 'open_access': 'true', + 'type': publication.get_cc_license_display(), + 'title': publication.get_cc_license_display(), + } + ], + 'language': ['EN'], + 'title': publication.in_journal.get_name_display(), + } return md @@ -234,3 +314,345 @@ class ReferenceForm(forms.ModelForm): ReferenceFormSet = modelformset_factory(Reference, formset=BaseReferenceFormSet, form=ReferenceForm, can_delete=True) + + +class DraftPublicationForm(forms.ModelForm): + """ + This Form is used by the Production Supervisors to create a new Publication object + and prefill all data. It is only able to create a `draft` version of a Publication object. + """ + class Meta: + model = Publication + fields = [ + 'doi_label', + 'pdf_file', + 'in_issue', + 'paper_nr', + 'title', + 'author_list', + 'abstract', + 'discipline', + 'domain', + 'subject_area', + 'secondary_areas', + 'cc_license', + 'BiBTeX_entry', + 'submission_date', + 'acceptance_date', + 'publication_date'] + + def __init__(self, data=None, arxiv_identifier_w_vn_nr=None, issue_id=None, *args, **kwargs): + # Use separate instance to be able to prefill the form without any existing Publication + self.submission = None + self.issue = None + self.to_journal = None + if arxiv_identifier_w_vn_nr: + try: + self.submission = Submission.objects.accepted().get( + arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + except Submission.DoesNotExist: + self.submission = None + + # Check if the Submission is related to a Journal with individual Publications only + if self.submission: + try: + self.to_journal = Journal.objects.has_individual_publications().get( + name=self.submission.submitted_to_journal) + except Journal.DoesNotExist: + self.to_journal = None + + # If the Journal is not for individual publications, choose a Issue for Publication + if issue_id and not self.to_journal: + try: + self.issue = self.get_possible_issues().get(id=issue_id) + except Issue.DoesNotExist: + self.issue = None + + super().__init__(data, *args, **kwargs) + + if kwargs.get('instance') or self.issue or self.to_journal: + # When updating: fix in_issue, because many fields are directly related to the issue. + del self.fields['in_issue'] + self.prefill_fields() + else: + self.fields['in_issue'].queryset = self.get_possible_issues() + self.delete_secondary_fields() + + def get_possible_issues(self): + issues = Issue.objects.filter(until_date__gte=timezone.now()) + if self.submission: + issues = issues.for_journal(self.submission.submitted_to_journal) + return issues + + def delete_secondary_fields(self): + """ + Delete fields from the self.fields dictionary. Later on, this submitted sparse form can + be used to prefill these secondary fields. + """ + del self.fields['doi_label'] + del self.fields['pdf_file'] + del self.fields['paper_nr'] + del self.fields['title'] + del self.fields['author_list'] + del self.fields['abstract'] + del self.fields['discipline'] + del self.fields['domain'] + del self.fields['subject_area'] + del self.fields['secondary_areas'] + del self.fields['cc_license'] + del self.fields['BiBTeX_entry'] + del self.fields['submission_date'] + del self.fields['acceptance_date'] + del self.fields['publication_date'] + + def clean(self): + data = super().clean() + if not self.instance.id: + if self.submission: + self.instance.accepted_submission = self.submission + if self.issue: + self.instance.in_issue = self.issue + if self.to_journal: + self.instance.in_journal = self.to_journal + return data + + def save(self, *args, **kwargs): + """ + Save the Publication object always as a draft and prefill the Publication with + related Submission data only when appending the Publication. + """ + do_prefill = False + if not self.instance.id: + do_prefill = True + super().save(*args, **kwargs) + if do_prefill: + self.first_time_fill() + return self.instance + + def first_time_fill(self): + """ + Take over fields from related Submission object. This can only be done after + the Publication object has been added to the database due to m2m relations. + """ + self.instance.status = STATUS_DRAFT + + if self.submission: + # Copy all existing author and non-author relations to Publication + for submission_author in self.submission.authors.all(): + PublicationAuthorsTable.objects.create( + publication=self.instance, contributor=submission_author) + self.instance.authors_claims.add(*self.submission.authors_claims.all()) + self.instance.authors_false_claims.add(*self.submission.authors_false_claims.all()) + + # Add Institutions to the publication related to the current authors + for author in self.instance.authors_registered.all(): + for current_affiliation in author.affiliations.active(): + self.instance.institutions.add(current_affiliation.institution) + + def prefill_fields(self): + if self.submission: + self.fields['title'].initial = self.submission.title + self.fields['author_list'].initial = self.submission.author_list + self.fields['abstract'].initial = self.submission.abstract + self.fields['discipline'].initial = self.submission.discipline + self.fields['domain'].initial = self.submission.domain + self.fields['subject_area'].initial = self.submission.subject_area + self.fields['secondary_areas'].initial = self.submission.secondary_areas + self.fields['submission_date'].initial = self.submission.submission_date + self.fields['acceptance_date'].initial = self.submission.acceptance_date + self.fields['publication_date'].initial = timezone.now() + + # Fill data that may be derived from the issue data + issue = None + if hasattr(self.instance, 'in_issue') and self.instance.in_issue: + issue = self.instance.in_issue + elif self.issue: + issue = self.issue + if issue: + self.prefill_with_issue(issue) + + # Fill data that may be derived from the issue data + journal = None + if hasattr(self.instance, 'in_journal') and self.instance.in_journal: + journal = self.instance.in_issue + elif self.to_journal: + journal = self.to_journal + if journal: + self.prefill_with_journal(journal) + + def prefill_with_issue(self, issue): + # Determine next available paper number: + paper_nr = Publication.objects.filter(in_issue__in_volume=issue.in_volume).count() + 1 + if paper_nr > 999: + raise PaperNumberingError(paper_nr) + self.fields['paper_nr'].initial = str(paper_nr) + doi_label = '{journal}.{vol}.{issue}.{paper}'.format( + journal=issue.in_volume.in_journal.name, + vol=issue.in_volume.number, + issue=issue.number, + paper=str(paper_nr).rjust(3, '0')) + self.fields['doi_label'].initial = doi_label + + doi_string = '10.21468/{doi}'.format(doi=doi_label) + bibtex_entry = ( + '@Article{%s,\n' + '\ttitle={{%s},\n' + '\tauthor={%s},\n' + '\tjournal={%s},\n' + '\tvolume={%i},\n' + '\tissue={%i},\n' + '\tpages={%i},\n' + '\tyear={%s},\n' + '\tpublisher={SciPost},\n' + '\tdoi={%s},\n' + '\turl={https://scipost.org/%s},\n' + '}' + ) % ( + doi_string, + self.submission.title, + self.submission.author_list.replace(',', ' and'), + issue.in_volume.in_journal.abbreviation_citation, + issue.in_volume.number, + issue.number, + paper_nr, + issue.until_date.strftime('%Y'), + doi_string, + doi_string) + self.fields['BiBTeX_entry'].initial = bibtex_entry + if not self.instance.BiBTeX_entry: + self.instance.BiBTeX_entry = bibtex_entry + + def prefill_with_journal(self, journal): + # Determine next available paper number: + paper_nr = journal.publications.count() + 1 + self.fields['paper_nr'].initial = str(paper_nr) + doi_label = '{journal}.{paper}'.format( + journal=journal.name, + paper=paper_nr) + self.fields['doi_label'].initial = doi_label + + doi_string = '10.21468/{doi}'.format(doi=doi_label) + bibtex_entry = ( + '@Article{%s,\n' + '\ttitle={{%s},\n' + '\tauthor={%s},\n' + '\tjournal={%s},\n' + '\tpages={%i},\n' + '\tyear={%s},\n' + '\tpublisher={SciPost},\n' + '\tdoi={%s},\n' + '\turl={https://scipost.org/%s},\n' + '}' + ) % ( + doi_string, + self.submission.title, + self.submission.author_list.replace(',', ' and'), + journal.abbreviation_citation, + paper_nr, + timezone.now().year, + doi_string, + doi_string) + self.fields['BiBTeX_entry'].initial = bibtex_entry + if not self.instance.BiBTeX_entry: + self.instance.BiBTeX_entry = bibtex_entry + + +class DraftPublicationApprovalForm(forms.ModelForm): + class Meta: + model = Publication + fields = () + + def save(self, commit=True): + self.instance.status = PUBLICATION_PREPUBLISHED + if commit: + self.instance.save() + mail_sender = DirectMailUtil(mail_code='publication_ready', instance=self.instance) + mail_sender.send() + return self.instance + + +class PublicationGrantsForm(forms.ModelForm): + grant = forms.ModelChoiceField(queryset=Grant.objects.none()) + + class Meta: + model = Publication + fields = [] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['grant'].queryset = Grant.objects.exclude( + id__in=self.instance.grants.values_list('id', flat=True)) + + def save(self, commit=True): + if commit: + self.instance.grants.add(self.cleaned_data['grant']) + return self.instance + + +class PublicationPublishForm(RequestFormMixin, forms.ModelForm): + class Meta: + model = Publication + fields = [] + + def move_pdf(self): + """ + To keep the Publication pdfs organized we move the pdfs to their own folder + organized by journal and optional issue folder. + """ + initial_path = self.instance.pdf_file.path + + new_dir = '' + if self.instance.in_issue: + new_dir += self.instance.in_issue.path + elif self.instance.in_journal: + new_dir += 'SCIPOST_JOURNALS/{name}'.format(name=self.instance.in_journal.name) + + new_dir += '/{paper_nr}'.format(paper_nr=self.instance.get_paper_nr()) + os.makedirs(settings.MEDIA_ROOT + new_dir) + + new_dir += '/{doi}.pdf'.format(doi=self.instance.doi_label.replace('.', '_')) + os.rename(initial_path, settings.MEDIA_ROOT + new_dir) + self.instance.pdf_file.name = new_dir + self.instance.status = PUBLICATION_PUBLISHED + self.instance.save() + + def update_submission(self): + # Mark the submission as having been published: + submission = self.instance.accepted_submission + submission.published_as = self.instance + submission.status = 'published' + submission.save() + + # Add SubmissionEvents + submission.add_general_event( + 'The Submission has been published as %s.' % self.instance.doi_label) + + def update_stream(self): + # Update ProductionStream + submission = self.instance.accepted_submission + if hasattr(submission, 'production_stream'): + stream = submission.production_stream + stream.status = PROOFS_PUBLISHED + stream.save() + if self.request.user.production_user: + prodevent = ProductionEvent( + stream=stream, + event='status', + comments=' published the manuscript.', + noted_by=self.request.user.production_user + ) + prodevent.save() + notify_stream_status_change(self.request.user, stream, False) + + def save(self, commit=True): + if commit: + self.move_pdf() + self.update_submission() + self.update_stream() + + # Email authors + JournalUtils.load({'publication': self.instance}) + JournalUtils.send_authors_paper_published_email() + notify_manuscript_published(self.request.user, self.instance, False) + + return self.instance diff --git a/journals/management/__init__.py b/journals/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/journals/management/commands/__init__.py b/journals/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/journals/management/commands/create_issues.py b/journals/management/commands/create_issues.py new file mode 100644 index 0000000000000000000000000000000000000000..ea07e6ac7ca831da3148b22110b9ca7e4a28576f --- /dev/null +++ b/journals/management/commands/create_issues.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Issue objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Issues to add') + + def handle(self, *args, **kwargs): + self.create_issues(kwargs['number']) + + def create_issues(self, n): + factories.IssueFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Issues.'.format(n=n))) diff --git a/journals/management/commands/create_journals.py b/journals/management/commands/create_journals.py new file mode 100644 index 0000000000000000000000000000000000000000..eaaa425050f9016ea0ed2ca9dde902b553bad262 --- /dev/null +++ b/journals/management/commands/create_journals.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Journal objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Journals to add') + + def handle(self, *args, **kwargs): + self.create_journals(kwargs['number']) + + def create_journals(self, n): + factories.JournalFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Journals.'.format(n=n))) diff --git a/journals/management/commands/create_publications.py b/journals/management/commands/create_publications.py new file mode 100644 index 0000000000000000000000000000000000000000..24c2f0480db625b119939ba394fb73d7f11901ab --- /dev/null +++ b/journals/management/commands/create_publications.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand + +from journals.constants import SCIPOST_JOURNALS_SUBMIT +from journals.factories import PublicationFactory + + +class Command(BaseCommand): + help = 'Create random Publication objects by using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of publications to add', + ) + parser.add_argument( + '--journal', choices=[i[0] for i in SCIPOST_JOURNALS_SUBMIT], + action='store', dest='journal', + help='The name of the specific Journal to add the Publications to', + ) + + def handle(self, *args, **kwargs): + if kwargs['number'] > 0: + journal = None + if kwargs.get('journal'): + journal = kwargs['journal'] + self.create_publications(kwargs['number'], journal=journal) + + def create_publications(self, n, journal=None): + PublicationFactory.create_batch(n, journal=journal) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Publications.'.format(n=n))) diff --git a/journals/management/commands/create_volumes.py b/journals/management/commands/create_volumes.py new file mode 100644 index 0000000000000000000000000000000000000000..29dce2f400ace4310f7996774503965260707425 --- /dev/null +++ b/journals/management/commands/create_volumes.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from journals import factories + + +class Command(BaseCommand): + help = 'Create Volume objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Volumes to add') + + def handle(self, *args, **kwargs): + self.create_volumes(kwargs['number']) + + def create_volumes(self, n): + factories.VolumeFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Volumes.'.format(n=n))) diff --git a/journals/managers.py b/journals/managers.py index 0b058c1b3a78d47baca5c366cf9d94c8a71e60a3..272410807103705bb5902cf2fbc225423d70ce7c 100644 --- a/journals/managers.py +++ b/journals/managers.py @@ -1,53 +1,58 @@ from django.db import models -from django.http import Http404 from django.utils import timezone -from .constants import STATUS_PUBLISHED, STATUS_DRAFT +from .constants import STATUS_PUBLISHED, STATUS_DRAFT, PUBLICATION_PUBLISHED, ISSUES_AND_VOLUMES,\ + ISSUES_ONLY, INDIVIDUAL_PUBLCATIONS -class JournalManager(models.Manager): +class JournalQuerySet(models.QuerySet): def active(self): return self.filter(active=True) + def has_issues(self): + return self.filter(structure__in=(ISSUES_AND_VOLUMES, ISSUES_ONLY)) -class IssueManager(models.Manager): - def get_published(self, *args, **kwargs): - try: - return self.published(*args, **kwargs)[0] - except IndexError: - raise Http404 + def has_individual_publications(self): + return self.filter(structure=INDIVIDUAL_PUBLCATIONS) - def published(self, journal=None, **kwargs): - issues = self.filter(status=STATUS_PUBLISHED, **kwargs) - if journal: - issues.filter(in_volume__in_journal=journal) - return issues - def in_draft(self, journal=None, **kwargs): - issues = self.filter(status=STATUS_DRAFT, **kwargs) - if journal: - issues.filter(in_volume__in_journal=journal) - return issues +class IssueQuerySet(models.QuerySet): + def published(self): + return self.filter(status=STATUS_PUBLISHED) - def get_current_issue(self, *args, **kwargs): - return self.published(start_date__lte=timezone.now(), - until_date__gte=timezone.now(), - **kwargs).order_by('-until_date').first() + def in_draft(self): + return self.filter(status=STATUS_DRAFT) - def get_last_filled_issue(self, *args, **kwargs): - return self.published(publication__isnull=False, - **kwargs).order_by('-until_date').first() + def for_journal(self, journal_name): + return self.filter( + models.Q(in_volume__in_journal__name=journal_name) | + models.Q(in_journal__name=journal_name)) + + def get_current_issue(self): + return self.published( + start_date__lte=timezone.now(), until_date__gte=timezone.now()).first() class PublicationQuerySet(models.QuerySet): - def get_published(self, *args, **kwargs): - try: - return self.published(*args, **kwargs)[0] - except IndexError: - raise Http404 + def published(self): + return self.filter(status=PUBLICATION_PUBLISHED).filter( + models.Q(in_issue__status=STATUS_PUBLISHED) | models.Q(in_journal__active=True)) + + def unpublished(self): + return self.exclude(status=PUBLICATION_PUBLISHED) + + def in_draft(self): + return self.filter(in_issue__status=STATUS_DRAFT) + + def drafts(self): + return self.filter(status=STATUS_DRAFT) - def published(self, **kwargs): - return self.filter(in_issue__status=STATUS_PUBLISHED, **kwargs) + def for_subject(self, subject_code): + return self.filter( + models.Q(subject_area=subject_code) | + models.Q(secondary_areas__contains=[subject_code])) - def in_draft(self, **kwargs): - return self.filter(in_issue__status=STATUS_DRAFT, **kwargs) + def for_journal(self, journal_name): + return self.filter( + models.Q(in_issue__in_volume__in_journal__name=journal_name) | + models.Q(in_journal__name=journal_name)) diff --git a/journals/migrations/0014_publication_status.py b/journals/migrations/0014_publication_status.py new file mode 100644 index 0000000000000000000000000000000000000000..0a10cb32d3d71e98d10230537c9ec76a05e038a6 --- /dev/null +++ b/journals/migrations/0014_publication_status.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-02 13:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0013_auto_20180216_0850'), + ] + + operations = [ + migrations.AddField( + model_name='publication', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('prepub', 'Pre-published'), ('pub', 'Published')], default='pub', max_length=8), + ), + ] diff --git a/journals/migrations/0015_auto_20180302_1404.py b/journals/migrations/0015_auto_20180302_1404.py new file mode 100644 index 0000000000000000000000000000000000000000..d1efbff642228e4db55409f7a6a2268b56672584 --- /dev/null +++ b/journals/migrations/0015_auto_20180302_1404.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-02 13:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0014_publication_status'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('prepub', 'Pre-published'), ('pub', 'Published')], default='draft', max_length=8), + ), + ] diff --git a/journals/migrations/0016_auto_20180303_0918.py b/journals/migrations/0016_auto_20180303_0918.py new file mode 100644 index 0000000000000000000000000000000000000000..f9448c4ab78a4ec88146dd6d6dd9d8a4cfd8fcb2 --- /dev/null +++ b/journals/migrations/0016_auto_20180303_0918.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-03 08:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def null_to_blank(apps, schema_editor): + Publication = apps.get_model('journals', 'Publication') + for pub in Publication.objects.all(): + if pub.BiBTeX_entry is None: + pub.BiBTeX_entry = '' + if pub.metadata_xml is None: + pub.metadata_xml = '' + pub.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0015_auto_20180302_1404'), + ] + + operations = [ + migrations.RunPython(null_to_blank), + migrations.AlterField( + model_name='publication', + name='BiBTeX_entry', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + migrations.AlterField( + model_name='publication', + name='metadata_xml', + field=models.TextField(blank=True, default=''), + preserve_default=False, + ), + ] diff --git a/journals/migrations/0017_auto_20180310_1103.py b/journals/migrations/0017_auto_20180310_1103.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ba40965f1d7c49a56cedc586a25357f5b8bd9b --- /dev/null +++ b/journals/migrations/0017_auto_20180310_1103.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0016_auto_20180303_0918'), + ] + + operations = [ + migrations.AddField( + model_name='journal', + name='_has_issues', + field=models.BooleanField(default=True, verbose_name='Use Issues to group Publications'), + ), + migrations.AddField( + model_name='journal', + name='_has_volumes', + field=models.BooleanField(default=True, verbose_name='Use Issues to group Publications (if True, the use of Issues is required)'), + ), + migrations.AlterField( + model_name='publication', + name='in_issue', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='journals.Issue'), + ), + ] diff --git a/journals/migrations/0018_auto_20180310_1112.py b/journals/migrations/0018_auto_20180310_1112.py new file mode 100644 index 0000000000000000000000000000000000000000..028c046820795fa14e37b882febfced9ab715ba5 --- /dev/null +++ b/journals/migrations/0018_auto_20180310_1112.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:12 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0017_auto_20180310_1103'), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='in_journal', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='journals.Journal'), + ), + migrations.AlterField( + model_name='issue', + name='in_volume', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='journals.Volume'), + ), + ] diff --git a/journals/migrations/0019_auto_20180310_1115.py b/journals/migrations/0019_auto_20180310_1115.py new file mode 100644 index 0000000000000000000000000000000000000000..71ac22ab4b8851c23c3db53b9e151429a6481334 --- /dev/null +++ b/journals/migrations/0019_auto_20180310_1115.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0018_auto_20180310_1112'), + ] + + operations = [ + migrations.AddField( + model_name='publication', + name='in_journal', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='journals.Journal'), + ), + migrations.AlterField( + model_name='publication', + name='in_issue', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='journals.Issue'), + ), + ] diff --git a/journals/migrations/0020_auto_20180310_1137.py b/journals/migrations/0020_auto_20180310_1137.py new file mode 100644 index 0000000000000000000000000000000000000000..3e800f5de4a291096dfd7f1a302503a578bfe937 --- /dev/null +++ b/journals/migrations/0020_auto_20180310_1137.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0019_auto_20180310_1115'), + ] + + operations = [ + migrations.RenameField( + model_name='journal', + old_name='_has_issues', + new_name='has_issues', + ), + migrations.RenameField( + model_name='journal', + old_name='_has_volumes', + new_name='has_volumes', + ), + migrations.AlterField( + model_name='issue', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.PROTECT, to='journals.Journal'), + ), + migrations.AlterField( + model_name='issue', + name='in_volume', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.PROTECT, to='journals.Volume'), + ), + migrations.AlterField( + model_name='publication', + name='in_issue', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='journals.Issue'), + ), + migrations.AlterField( + model_name='publication', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='journals.Journal'), + ), + ] diff --git a/journals/migrations/0021_auto_20180310_1137.py b/journals/migrations/0021_auto_20180310_1137.py new file mode 100644 index 0000000000000000000000000000000000000000..70c8a6fd2ebcc3628790bf1de87db5743ac33bd8 --- /dev/null +++ b/journals/migrations/0021_auto_20180310_1137.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:37 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0020_auto_20180310_1137'), + ] + + operations = [ + migrations.AlterField( + model_name='journal', + name='has_volumes', + field=models.BooleanField(default=True, verbose_name='Use Volumes to group Publications'), + ), + ] diff --git a/journals/migrations/0022_auto_20180310_1154.py b/journals/migrations/0022_auto_20180310_1154.py new file mode 100644 index 0000000000000000000000000000000000000000..94624cc27b658c23a24d5cc75362314c9686ffd9 --- /dev/null +++ b/journals/migrations/0022_auto_20180310_1154.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 10:54 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0021_auto_20180310_1137'), + ] + + operations = [ + migrations.RemoveField( + model_name='journal', + name='has_issues', + ), + migrations.RemoveField( + model_name='journal', + name='has_volumes', + ), + ] diff --git a/journals/migrations/0023_journal_structure.py b/journals/migrations/0023_journal_structure.py new file mode 100644 index 0000000000000000000000000000000000000000..0bfb0012cc2548ce1d1db0a428b2ac810d4fcb9b --- /dev/null +++ b/journals/migrations/0023_journal_structure.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 11:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0022_auto_20180310_1154'), + ] + + operations = [ + migrations.AddField( + model_name='journal', + name='structure', + field=models.CharField(choices=[('IV', 'Issues and Volumes'), ('IO', 'Issues only'), ('IP', 'Individual Publications')], default='IV', max_length=2), + ), + ] diff --git a/journals/migrations/0024_auto_20180310_1740.py b/journals/migrations/0024_auto_20180310_1740.py new file mode 100644 index 0000000000000000000000000000000000000000..70cd9bdf95702d28414fd9199a8389fa51e40c68 --- /dev/null +++ b/journals/migrations/0024_auto_20180310_1740.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-10 16:40 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0023_journal_structure'), + ] + + operations = [ + migrations.AlterModelOptions( + name='issue', + options={'ordering': ('-until_date',)}, + ), + migrations.AlterModelOptions( + name='publication', + options={'ordering': ('-publication_date', '-paper_nr')}, + ), + migrations.AlterField( + model_name='issue', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issues', to='journals.Journal'), + ), + migrations.AlterField( + model_name='issue', + name='in_volume', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='issues', to='journals.Volume'), + ), + migrations.AlterField( + model_name='journal', + name='structure', + field=models.CharField(choices=[('IV', 'Issues and Volumes'), ('IP', 'Individual Publications')], default='IV', max_length=2), + ), + migrations.AlterField( + model_name='publication', + name='doi_label', + field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^[a-zA-Z]+.[0-9]+(.[0-9]+.[0-9]{3,})?$', 'Only valid DOI expressions are allowed (`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')]), + ), + migrations.AlterField( + model_name='volume', + name='in_journal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='journals.Journal'), + ), + ] diff --git a/journals/migrations/0025_auto_20180314_1637.py b/journals/migrations/0025_auto_20180314_1637.py new file mode 100644 index 0000000000000000000000000000000000000000..35b306e63723cd7b0b0ec6db28a0d0c5317b7c59 --- /dev/null +++ b/journals/migrations/0025_auto_20180314_1637.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 15:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0024_auto_20180310_1740'), + ] + + operations = [ + migrations.AlterField( + model_name='issue', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='journals.Journal'), + ), + migrations.AlterField( + model_name='issue', + name='in_volume', + field=models.ForeignKey(blank=True, help_text='Assign either an Volume or Journal to the Issue', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='journals.Volume'), + ), + migrations.AlterField( + model_name='publication', + name='in_issue', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='journals.Issue'), + ), + migrations.AlterField( + model_name='publication', + name='in_journal', + field=models.ForeignKey(blank=True, help_text='Assign either an Issue or Journal to the Publication', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='journals.Journal'), + ), + ] diff --git a/journals/mixins.py b/journals/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..730d920a08ce0f4947af6da6376019dae9ec175e --- /dev/null +++ b/journals/mixins.py @@ -0,0 +1,22 @@ +from .models import Publication + +from scipost.mixins import PermissionsMixin + + +class PublicationMixin: + model = Publication + slug_field = slug_url_kwarg = 'doi_label' + + +class ProdSupervisorPublicationPermissionMixin(PermissionsMixin): + """ + This will give permission to Production Supervisors if Publication is in_draft. + If Publication is not in draft, it will only give permission to administrators. + """ + permission_required = 'scipost.can_draft_publication' + + def has_permission(self): + has_perm = super().has_permission() + if has_perm and self.get_object().is_draft: + return True + return self.request.user.has_perm('scipost.can_publish_accepted_submission') diff --git a/journals/models.py b/journals/models.py index 31c9cdc87eec453db0c84da6cf35349172b8c8ee..3a859cb3fd501077c1b6163911d5aa3a76a77c2d 100644 --- a/journals/models.py +++ b/journals/models.py @@ -1,6 +1,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import JSONField +from django.core.exceptions import ValidationError from django.db import models from django.db.models import Avg, F from django.utils import timezone @@ -9,10 +10,11 @@ from django.urls import reverse from .behaviors import doi_journal_validator, doi_volume_validator,\ doi_issue_validator, doi_publication_validator from .constants import SCIPOST_JOURNALS, SCIPOST_JOURNALS_DOMAINS,\ - STATUS_DRAFT, STATUS_PUBLISHED, ISSUE_STATUSES,\ - CCBY4, CC_LICENSES, CC_LICENSES_URI + STATUS_DRAFT, STATUS_PUBLISHED, ISSUE_STATUSES, PUBLICATION_PUBLISHED,\ + CCBY4, CC_LICENSES, CC_LICENSES_URI, PUBLICATION_STATUSES,\ + JOURNAL_STRUCTURE, ISSUES_AND_VOLUMES, ISSUES_ONLY from .helpers import paper_nr_string, journal_name_abbrev_citation -from .managers import IssueManager, PublicationQuerySet, JournalManager +from .managers import IssueQuerySet, PublicationQuerySet, JournalQuerySet from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS from scipost.fields import ChoiceArrayField @@ -71,27 +73,56 @@ class PublicationAuthorsTable(models.Model): class Journal(models.Model): + """ + Journal is a container of Publications with a unique issn and doi_label. + Publications may be categorized into issues or issues and volumes. + """ name = models.CharField(max_length=100, choices=SCIPOST_JOURNALS, unique=True) doi_label = models.CharField(max_length=200, unique=True, db_index=True, validators=[doi_journal_validator]) issn = models.CharField(max_length=16, default='2542-4653', blank=True) active = models.BooleanField(default=True) + structure = models.CharField(max_length=2, + choices=JOURNAL_STRUCTURE, default=ISSUES_AND_VOLUMES) - objects = JournalManager() + objects = JournalQuerySet.as_manager() def __str__(self): return self.get_name_display() + def get_absolute_url(self): + return reverse('scipost:landing_page', args=(self.doi_label,)) + @property def doi_string(self): return '10.21468/' + self.doi_label - def get_absolute_url(self): - return reverse('scipost:landing_page', args=[self.doi_label]) + @property + def has_issues(self): + return self.structure in (ISSUES_AND_VOLUMES, ISSUES_ONLY) - def get_abbreviation_citation(self): + @property + def has_volumes(self): + return self.structure in (ISSUES_AND_VOLUMES) + + @property + def abbreviation_citation(self): return journal_name_abbrev_citation(self.name) + def get_issues(self): + if self.structure == ISSUES_AND_VOLUMES: + return Issue.objects.filter(in_volume__in_journal=self) + elif self.structure == ISSUES_ONLY: + return self.issues.all() + return Issue.objects.none() + + def get_publications(self): + if self.structure == ISSUES_AND_VOLUMES: + return Publication.objects.filter(in_issue__in_volume__in_journal=self) + elif self.structure == ISSUES_ONLY: + return Publication.objects.filter(in_issue__in_journal=self) + return self.publications.all() + def nr_publications(self, tier=None): publications = Publication.objects.filter(in_issue__in_volume__in_journal=self) if tier: @@ -125,6 +156,9 @@ class Journal(models.Model): class Volume(models.Model): + """ + A Volume may be used as a subgroup of Publications related to a specific Issue object. + """ in_journal = models.ForeignKey('journals.Journal', on_delete=models.CASCADE) number = models.PositiveSmallIntegerField() start_date = models.DateField(default=timezone.now) @@ -133,11 +167,22 @@ class Volume(models.Model): validators=[doi_volume_validator]) class Meta: + default_related_name = 'volumes' unique_together = ('number', 'in_journal') def __str__(self): return str(self.in_journal) + ' Vol. ' + str(self.number) + def clean(self): + """ + Check if the Volume is assigned to a valid Journal. + """ + if not self.in_journal.has_volumes: + raise ValidationError({ + 'in_journal': ValidationError('This journal does not allow for the use of Volumes', + code='invalid'), + }) + @property def doi_string(self): return '10.21468/' + self.doi_label @@ -175,30 +220,59 @@ class Volume(models.Model): class Issue(models.Model): - in_volume = models.ForeignKey('journals.Volume', on_delete=models.CASCADE) + """ + An Issue may be used as a subgroup of Publications related to a specific Journal object. + """ + in_journal = models.ForeignKey( + 'journals.Journal', on_delete=models.CASCADE, null=True, blank=True, + help_text='Assign either an Volume or Journal to the Issue') + in_volume = models.ForeignKey( + 'journals.Volume', on_delete=models.CASCADE, null=True, blank=True, + help_text='Assign either an Volume or Journal to the Issue') number = models.PositiveSmallIntegerField() start_date = models.DateField(default=timezone.now) until_date = models.DateField(default=timezone.now) status = models.CharField(max_length=20, choices=ISSUE_STATUSES, default=STATUS_PUBLISHED) doi_label = models.CharField(max_length=200, unique=True, db_index=True, validators=[doi_issue_validator]) + # absolute path on filesystem: (JOURNALS_DIR)/journal/vol/issue/ path = models.CharField(max_length=200) - objects = IssueManager() + objects = IssueQuerySet.as_manager() class Meta: + default_related_name = 'issues' + ordering = ('-until_date',) unique_together = ('number', 'in_volume') def __str__(self): text = self.issue_number if hasattr(self, 'proceedings'): return text - text += self.period_as_string() + text += ' (%s)' % self.period_as_string if self.status == STATUS_DRAFT: text += ' (In draft)' return text + def clean(self): + """ + Check if either a Journal or Volume is assigned to the Issue, else the Issue be floating + like Musk's red Roadster. + """ + if not (self.in_journal or self.in_volume): + raise ValidationError({ + 'in_journal': ValidationError('Either assign a Journal or Volume to this Issue', + code='required'), + 'in_volume': ValidationError('Either assign a Journal or Volume to this Issue', + code='required'), + }) + if self.in_journal and not self.in_journal.has_issues: + raise ValidationError({ + 'in_journal': ValidationError('This journal does not allow for the use of Issues', + code='invalid'), + }) + def get_absolute_url(self): return reverse('scipost:issue_detail', args=[self.doi_label]) @@ -210,15 +284,15 @@ class Issue(models.Model): def issue_number(self): return '%s issue %s' % (self.in_volume, self.number) + @property def short_str(self): return 'Vol. %s issue %s' % (self.in_volume.number, self.number) + @property def period_as_string(self): if self.start_date.month == self.until_date.month: - return ' (%s %s)' % (self.until_date.strftime('%B'), self.until_date.strftime('%Y')) - else: - return (' (' + self.start_date.strftime('%B') + '-' + self.until_date.strftime('%B') + - ' ' + self.until_date.strftime('%Y') + ')') + return '%s %s' % (self.until_date.strftime('%B'), self.until_date.strftime('%Y')) + return '%s - %s' % (self.start_date.strftime('%B'), self.until_date.strftime('%B %Y')) def is_current(self): return self.start_date <= timezone.now().date() and\ @@ -260,12 +334,21 @@ class Publication(models.Model): """ A Publication is an object directly related to an accepted Submission. It contains metadata, the actual publication file, author data, etc. etc. + + It may be directly related to a Journal or to an Issue. """ # Publication data accepted_submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE, related_name='publication') - in_issue = models.ForeignKey('journals.Issue', on_delete=models.CASCADE) + in_issue = models.ForeignKey( + 'journals.Issue', on_delete=models.CASCADE, null=True, blank=True, + help_text='Assign either an Issue or Journal to the Publication') + in_journal = models.ForeignKey( + 'journals.Journal', on_delete=models.CASCADE, null=True, blank=True, + help_text='Assign either an Issue or Journal to the Publication') paper_nr = models.PositiveSmallIntegerField() + status = models.CharField(max_length=8, + choices=PUBLICATION_STATUSES, default=STATUS_DRAFT) # Core fields title = models.CharField(max_length=300) @@ -282,14 +365,12 @@ class Publication(models.Model): # Authors authors_registered = models.ManyToManyField('scipost.Contributor', blank=True, through='PublicationAuthorsTable', - through_fields=('publication', 'contributor'), - related_name='publications') + through_fields=('publication', 'contributor')) authors_unregistered = models.ManyToManyField('journals.UnregisteredAuthor', blank=True, through='PublicationAuthorsTable', through_fields=( 'publication', - 'unregistered_author'), - related_name='publications') + 'unregistered_author')) authors_claims = models.ManyToManyField('scipost.Contributor', blank=True, related_name='claimed_publications') authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True, @@ -298,19 +379,17 @@ class Publication(models.Model): cc_license = models.CharField(max_length=32, choices=CC_LICENSES, default=CCBY4) # Funders - grants = models.ManyToManyField('funders.Grant', blank=True, related_name="publications") - funders_generic = models.ManyToManyField( - 'funders.Funder', blank=True, related_name="publications") # not linked to a grant - institutions = models.ManyToManyField('affiliations.Institution', - blank=True, related_name="publications") + grants = models.ManyToManyField('funders.Grant', blank=True) + funders_generic = models.ManyToManyField('funders.Funder', blank=True) # not linked to a grant + institutions = models.ManyToManyField('affiliations.Institution', blank=True) # Metadata metadata = JSONField(default={}, blank=True, null=True) - metadata_xml = models.TextField(blank=True, null=True) # for Crossref deposit + metadata_xml = models.TextField(blank=True) # for Crossref deposit metadata_DOAJ = JSONField(default={}, blank=True, null=True) doi_label = models.CharField(max_length=200, unique=True, db_index=True, validators=[doi_publication_validator]) - BiBTeX_entry = models.TextField(blank=True, null=True) + BiBTeX_entry = models.TextField(blank=True) doideposit_needs_updating = models.BooleanField(default=False) citedby = JSONField(default={}, blank=True, null=True) @@ -324,14 +403,53 @@ class Publication(models.Model): objects = PublicationQuerySet.as_manager() + class Meta: + default_related_name = 'publications' + ordering = ('-publication_date', '-paper_nr') + def __str__(self): - header = (self.citation() + ', ' - + self.title[:30] + ' by ' + self.author_list[:30] - + ', published ' + self.publication_date.strftime('%Y-%m-%d')) - return header + return '{cite}, {title} by {authors}, {date}'.format( + cite=self.citation, + title=self.title[:30], + authors=self.author_list[:30], + date=self.publication_date.strftime('%Y-%m-%d')) + + def clean(self): + """ + Check if either a valid Journal or Issue is assigned to the Publication. + """ + if not (self.in_journal or self.in_issue): + raise ValidationError({ + 'in_journal': ValidationError( + 'Either assign a Journal or Issue to this Publication', code='required'), + 'in_issue': ValidationError( + 'Either assign a Journal or Issue to this Publication', code='required'), + }) + if self.in_journal and self.in_issue: + # Assigning both a Journal and an Issue will screw up the database + raise ValidationError({ + 'in_journal': ValidationError( + 'Either assign only a Journal or Issue to this Publication', code='invalid'), + 'in_issue': ValidationError( + 'Either assign only a Journal or Issue to this Publication', code='invalid'), + }) + if self.in_issue and not self.in_issue.in_volume.in_journal.has_issues: + # Assigning both a Journal and an Issue will screw up the database + raise ValidationError({ + 'in_issue': ValidationError( + 'This journal does not allow the use of Issues', + code='invalid'), + }) + if self.in_journal and self.in_journal.has_issues: + # Assigning both a Journal and an Issue will screw up the database + raise ValidationError({ + 'in_journal': ValidationError( + 'This journal does not allow the use of individual Publications', + code='invalid'), + }) def get_absolute_url(self): - return reverse('scipost:publication_detail', args=[self.doi_label]) + return reverse('scipost:publication_detail', args=(self.doi_label,)) def get_cc_license_URI(self): for (key, val) in CC_LICENSES_URI: @@ -343,14 +461,64 @@ class Publication(models.Model): def doi_string(self): return '10.21468/' + self.doi_label - def get_paper_nr(self): - return paper_nr_string(self.paper_nr) + @property + def is_draft(self): + return self.status == STATUS_DRAFT + + @property + def is_published(self): + if self.status != PUBLICATION_PUBLISHED: + return False + + if self.in_issue: + return self.in_issue.status == STATUS_PUBLISHED + elif self.in_journal: + return self.in_journal.active + return False + + @property + def has_xml_metadata(self): + return self.metadata_xml != '' + @property + def has_bibtex_entry(self): + return self.BiBTeX_entry != '' + + @property + def has_citation_list(self): + return 'citation_list' in self.metadata and len(self.metadata['citation_list']) > 0 + + @property + def has_funding_statement(self): + return 'funding_statement' in self.metadata and self.metadata['funding_statement'] + + @property def citation(self): - return (self.in_issue.in_volume.in_journal.get_abbreviation_citation() - + ' ' + str(self.in_issue.in_volume.number) - + ', ' + self.get_paper_nr() - + ' (' + self.publication_date.strftime('%Y') + ')') + """ + Return Publication name in the preferred citation format. + """ + if self.in_issue: + return '{journal} {volume}, {paper_nr} ({year})'.format( + journal=self.in_issue.in_volume.in_journal.abbreviation_citation, + volume=self.in_issue.in_volume.number, + paper_nr=self.get_paper_nr(), + year=self.publication_date.strftime('%Y')) + elif self.in_journal: + return '{journal}, {paper_nr} ({year})'.format( + journal=self.in_journal.abbreviation_citation, + paper_nr=self.paper_nr, + year=self.publication_date.strftime('%Y')) + return '{paper_nr} ({year})'.format( + paper_nr=self.paper_nr, + year=self.publication_date.strftime('%Y')) + + def get_journal(self): + return self.in_journal or self.in_issue.in_volume.in_journal + + def get_paper_nr(self): + if self.in_journal: + return self.paper_nr + return paper_nr_string(self.paper_nr) def citation_rate(self): """ diff --git a/journals/search_indexes.py b/journals/search_indexes.py index 08c3b2898fcfc8613a8e4eb8d48a398ca914d87f..89035e4731cb1cd2644d7c25225f5ca29ec414d0 100644 --- a/journals/search_indexes.py +++ b/journals/search_indexes.py @@ -1,5 +1,3 @@ -# import datetime - from haystack import indexes from .models import Publication diff --git a/journals/signals.py b/journals/signals.py new file mode 100644 index 0000000000000000000000000000000000000000..01787e588d96919588b5c32ed3f9dbaf4256b46f --- /dev/null +++ b/journals/signals.py @@ -0,0 +1,17 @@ +from django.contrib.auth.models import User, Group + +from notifications.signals import notify + + +def notify_manuscript_published(sender, instance, created, **kwargs): + """ + Notify the authors about their new Publication. + + instance -- Publication instance + """ + if instance.is_published: + authors = User.objects.filter(contributor__publications=instance) + editorial_administration = Group.objects.get(name='Editorial Administrators') + for user in authors: + notify.send(sender=sender, recipient=user, actor=editorial_administration, + verb=' published your manuscript.', target=instance) diff --git a/journals/templates/journals/_base.html b/journals/templates/journals/_base.html index bd692680e7e564e05329bdc90c1314e1ac27c0b5..b1fc60fa1e6253c2881e2aec50d3eb9398080fd0 100644 --- a/journals/templates/journals/_base.html +++ b/journals/templates/journals/_base.html @@ -2,8 +2,8 @@ {% load staticfiles %} -{% block pagetitle %}: {{journal}}{% endblock pagetitle %} -{% block body_class %}{{block.super}} journals{% endblock %} +{% block pagetitle %}: {{ journal }}{% endblock pagetitle %} +{% block body_class %}{{ block.super }} journals{% endblock %} {% block breadcrumb %} <div class="container-outside breadcrumb-nav"> @@ -21,11 +21,15 @@ <div class="container mt-3"> <div class="row"> <div class="col journal"> - <h2 class="banner d-inline-block mr-2"><a href="{% url 'scipost:landing_page' journal.doi_label %}">{{journal}}</a></h2> + <h2 class="banner d-inline-block mr-2 mb-0"><a href="{{ journal.get_absolute_url }}">{{journal}}</a></h2> <ul class="links"> {% if journal.active or request.user.is_staff %} - <li><a class="{% block link_class_physics_issues %}{% endblock %}" href="{% url 'journal:issues' journal.doi_label %}">Issues</a></li> - <li><a class="{% block link_class_physics_recent %}{% endblock %}" href="{% url 'journal:recent' journal.doi_label %}">Recent</a></li> + {% if journal.has_issues %} + <li><a class="{% block link_class_physics_issues %}{% endblock %}" href="{% url 'journal:issues' journal.doi_label %}">Issues</a></li> + <li><a class="recent {% block link_class_physics_recent %}{% endblock %}" href="{% url 'journal:recent' journal.doi_label %}">Recent</a></li> + {% else %} + <li><a class="recent" href="{{ journal.get_absolute_url }}">Recent</a></li> + {% endif %} <li><a class="{% block link_class_physics_accepted %}{% endblock %}" href="{% url 'journal:accepted' journal.doi_label %}">Accepted</a></li> <li><a href="{% url 'submissions:submissions' %}?to_journal={{ journal.name }}">Submissions</a></li> <li><a class="{% block link_class_physics_info %}{% endblock %}" href="{% url 'journal:info_for_authors' journal.doi_label %}">Info for authors</a></li> diff --git a/journals/templates/journals/add_author.html b/journals/templates/journals/add_author.html index 668919333f224cee185775fc77edd28e4510821e..627dd1a06baf3690764cdcf7ec3ab42063044c62 100644 --- a/journals/templates/journals/add_author.html +++ b/journals/templates/journals/add_author.html @@ -7,8 +7,6 @@ <div class="container"> <nav class="breadcrumb hidden-sm-down"> <a href="{% url 'journals:journals' %}" class="breadcrumb-item">Journals</a> - <a href="{{publication.in_issue.in_volume.in_journal.get_absolute_url}}" class="breadcrumb-item">{{publication.in_issue.in_volume.in_journal}}</a> - <a href="{{publication.in_issue.get_absolute_url}}" class="breadcrumb-item">{{publication.in_issue.short_str}}</a> <a href="{{publication.get_absolute_url}}" class="breadcrumb-item">{{publication.citation}}</a> <span class="breadcrumb-item active">Add author to publication</span> diff --git a/journals/templates/journals/create_citation_list_metadata.html b/journals/templates/journals/create_citation_list_metadata.html index c6f5f6d5f79e130b4ed33a83ce96a1371c370480..724637a48de011831f16a8f857cd0ee4fd414e28 100644 --- a/journals/templates/journals/create_citation_list_metadata.html +++ b/journals/templates/journals/create_citation_list_metadata.html @@ -22,15 +22,14 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Create citation list metadata page for <a href="{{ publication.get_absolute_url }}">{{ publication.doi_label }}</a></h1> - </div> -</div> - + <p> + The following field is prefilled with the current citation list of the Publication object. Once you submit, it will overwrite the current citation list, shown below. + </p> + <br> -<div class="row"> - <div class="col-12"> <form action="{% url 'journals:create_citation_list_metadata' publication.doi_label %}" method="post"> {% csrf_token %} - {{ bibitems_form|bootstrap }} + {{ form|bootstrap }} <input type="submit" class="btn btn-primary" value="Submit"> <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}" class="ml-3 btn btn-link">Back to Admin for {{ publication.doi_label }}</a> </form> @@ -38,17 +37,14 @@ <hr class="divider"> <h3>Current citation list metadata:</h3> + <br> <table class="table"> - {% for citation in citation_list %} + {% for citation in publication.metadata.citation_list %} <tr> <td>{{ citation.key }}</td><td>{{ citation.doi }}</td> </tr> {% endfor %} </table> - - <hr> - - <p>Once you're happy with this metadata, you can <a href="{{publication.get_absolute_url}}">return to the publication's page</a> or to the <a href="{% url 'journals:manage_metadata' %}">metadata management page</a> or to <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">this publication's metadata management page</a></p> </div> </div> diff --git a/journals/templates/journals/create_funding_info_metadata.html b/journals/templates/journals/create_funding_info_metadata.html index 2373370f7b3d9ca053ec85ace517e5e520b3e775..43c046b30c36fe66ce7be51270f324f28d50bd1c 100644 --- a/journals/templates/journals/create_funding_info_metadata.html +++ b/journals/templates/journals/create_funding_info_metadata.html @@ -22,32 +22,21 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Create funding info metadata page for <a href="{{ publication.get_absolute_url }}">{{ publication.doi_label }}</a></h1> - </div> -</div> - - -<div class="row"> - <div class="col-12"> - - {% if errormessage %} - <h2 class="text-danger">{{ errormessage }}</h2> - {% endif %} + <p> + The following field is prefilled with the current funding info of the Publication object. Once you submit, it will overwrite the current funding info, shown below. + </p> + <br> <form method="post"> {% csrf_token %} - {{ funding_info_form|bootstrap }} + {{ form|bootstrap }} <input type="submit" class="btn btn-primary" value="Submit"> <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}" class="ml-3 btn btn-link">Back to Admin for {{ publication.doi_label }}</a> </form> <hr class="divider"> <h3>Current funding info metadata:</h3> - <p>{{ funding_statement|linebreaksbr }}</p> - - <hr> - - <p class="mb-0">Once you're happy with this metadata, you can <a href="{{publication.get_absolute_url}}">return to the publication's page</a> or to the <a href="{% url 'journals:manage_metadata' %}">metadata management page</a> or to <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">this publication's metadata management page</a></p> - + <p class="mb-0">{{ publication.metadata.funding_statement|linebreaksbr }}</p> </div> </div> diff --git a/journals/templates/journals/create_metadata_xml.html b/journals/templates/journals/create_metadata_xml.html index a6627c2567926544804cc917b2105849d0ee91b6..dff180d06c8f5142f944b9bd3121165b7d9f74a7 100644 --- a/journals/templates/journals/create_metadata_xml.html +++ b/journals/templates/journals/create_metadata_xml.html @@ -22,18 +22,13 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Create metadata XML (for Crossref deposit)</h1> - </div> -</div> - -<div class="row"> - <div class="col-12"> - {% if errormessage %} - <h2 class="text-danger">{{ errormessage }}</h2> - {% endif %} - + <p> + The following field is prefilled with data from the Publication object. Once you accept them, they will overwrite the current metadata, shown below. + </p> + <br> <form action="{% url 'journals:create_metadata_xml' publication.doi_label %}" method="post"> {% csrf_token %} - {{ create_metadata_xml_form|bootstrap }} + {{ form|bootstrap }} <input type="submit" class="btn btn-primary" value="Accept the metadata"> <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}" class="ml-3 btn btn-link">Back to Admin for {{ publication.doi_label }}</a> </form> @@ -43,9 +38,6 @@ <h3>Current metadata xml</h3> <br> <pre><code>{{ publication.metadata_xml|linebreaksbr }}</code></pre> - <br> - <p class="mb-0">Once you're happy with this metadata, you can <a href="{{publication.get_absolute_url}}">return to the publication's page</a> or to the <a href="{% url 'journals:manage_metadata' %}">metadata management page</a></p> - </div> </div> diff --git a/journals/templates/journals/grants_form.html b/journals/templates/journals/grants_form.html new file mode 100644 index 0000000000000000000000000000000000000000..75c4ed4f2488b3a92f707bde1c93d2c21e2ee38c --- /dev/null +++ b/journals/templates/journals/grants_form.html @@ -0,0 +1,34 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Publication related grant(s){% endblock pagetitle %} + +{% block content %} + + +<h1>Publication related grant(s)</h1> + + +<h3>Add existing grant to this Publication</h3> +<form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Add"> + <a class="btn btn-link ml-2" href="{% url 'funders:add_grant' %}">Create new Grant</a> +</form> + +<hr class="divider"> + + +<h3>Current grant(s)</h3> +<ul> + {% for grant in publication.grants.all %} + <li>{{ grant }} - <a class="text-danger" href="{% url 'journals:remove_grant' form.instance.doi_label grant.id %}">Remove grant from Publication</a></li> + {% empty %} + <li><em>No grants added</em></li> + {% endfor %} +</ul> + + +{% endblock %} diff --git a/journals/templates/journals/initiate_publication.html b/journals/templates/journals/initiate_publication.html deleted file mode 100644 index c9fa7acc87fb2a42b4829a2ce0363c7bec8d1863..0000000000000000000000000000000000000000 --- a/journals/templates/journals/initiate_publication.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% load bootstrap %} - -{% block pagetitle %}: Initiate publication{% endblock pagetitle %} - -{% block content %} - - -<h1>Initiate publication page</h1> - -<form action="{% url 'journals:initiate_publication' %}" method="post" enctype="multipart/form-data"> - {% csrf_token %} - {{ initiate_publication_form|bootstrap }} - <input type="submit" class="btn btn-primary" value="Submit"> -</form> - - - - -{% endblock %} diff --git a/journals/templates/journals/journal_accepted.html b/journals/templates/journals/journal_accepted.html index 2eb16483da99aa3932b753af4b69e555a40c39af..d5944bf769b67b5b121d54bf598176827ca56b11 100644 --- a/journals/templates/journals/journal_accepted.html +++ b/journals/templates/journals/journal_accepted.html @@ -19,7 +19,7 @@ <div class="row"> <div class="col-12"> <ul class="list-group list-group-flush"> - {% for submission in accepted_SP_submissions %} + {% for submission in accepted_submissions %} <li class="list-group-item"> <div class="card-body px-0"> {% include 'partials/submissions/submission_card_content.html' with submission=submission %} diff --git a/journals/templates/journals/journal_issue_detail.html b/journals/templates/journals/journal_issue_detail.html index 6b646613d4d4de29b67a170ad2c1d32a6e20dbf2..5b22c11f7d028b9f06f49c3351e72c7f33dd9193 100644 --- a/journals/templates/journals/journal_issue_detail.html +++ b/journals/templates/journals/journal_issue_detail.html @@ -14,10 +14,10 @@ {% block journal_header_block %} {% if prev_issue %} - <h4 class="d-inline-block"><a href="{{prev_issue.get_absolute_url}}">< Previous issue | {{prev_issue.short_str}}</a></h4> + <h4 class="d-inline-block"><a href="{{prev_issue.get_absolute_url}}"><i class="fa fa-long-arrow-left"></i> Previous issue | {{prev_issue.short_str}}</a></h4> {% endif %} {% if next_issue %} - <h4 class="float-right d-inline-block"><a href="{{next_issue.get_absolute_url}}">{{next_issue.short_str}} | Next issue ></a></h4> + <h4 class="float-right d-inline-block"><a href="{{next_issue.get_absolute_url}}">{{next_issue.short_str}} | Next issue <i class="fa fa-long-arrow-right"></i></a></h4> {% endif %} {% endblock %} diff --git a/journals/templates/journals/journal_issues.html b/journals/templates/journals/journal_issues.html index 601a8c550639617c5f3d130bbeaba8a83af987e0..e69b81f25d844551d18d925c6733a703ba2bb29a 100644 --- a/journals/templates/journals/journal_issues.html +++ b/journals/templates/journals/journal_issues.html @@ -18,7 +18,7 @@ <div class="row"> <div class="col-12"> <ul> - {% for issue in issues %} + {% for issue in journal.get_issues %} <li> <a href="{{issue.get_absolute_url}}">{{issue}}</a> {% if issue.proceedings %} diff --git a/journals/templates/journals/journal_landing_page.html b/journals/templates/journals/journal_landing_page.html index 1cd761620af9cf7acc1489e50227f2a4c8443e1c..98bae00dcc384a1fb6ab18f1d927f24e23eb15c0 100644 --- a/journals/templates/journals/journal_landing_page.html +++ b/journals/templates/journals/journal_landing_page.html @@ -7,62 +7,92 @@ {% block content %} - {% if current_issue %} - <div class="row"> - <div class="col-12"> - <h2 class="highlight-empty text-blue m-0 p-0 pt-2">Current issue: Vol. {{ current_issue.in_volume.number }} issue {{ current_issue.number }} (in progress)</h2> - {% if prev_issue %} - <h4 class="d-inline-block"><a href="{{prev_issue.get_absolute_url}}">< Previous issue | Vol. {{prev_issue.in_volume.number}} issue {{prev_issue.number}}</a></h4> - {% endif %} + {% if journal.has_issues %} + {% if current_issue %} + <div class="row"> + <div class="col-12"> + <h2 class="highlight-empty text-blue m-0 p-0 pt-2">Current issue: Vol. {{ current_issue.in_volume.number }} issue {{ current_issue.number }} (in progress)</h2> + {% if prev_issue %} + <h4 class="d-inline-block"><a href="{{prev_issue.get_absolute_url}}"><i class="fa fa-long-arrow-left"></i> Previous issue | Vol. {{prev_issue.in_volume.number}} issue {{prev_issue.number}}</a></h4> + {% endif %} + </div> </div> - </div> - <div class="row"> - <div class="col-12"> - <ul class="list-unstyled"> - {% for paper in current_issue.publication_set.all|dictsort:"paper_nr" %} - <li> - {% include 'partials/journals/publication_card.html' with publication=paper %} - </li> - {% empty %} - <li> - <h3>No publications found for this issue</h3> - </li> - {% endfor %} - </ul> + <div class="row"> + <div class="col-12"> + <ul class="list-unstyled"> + {% for paper in current_issue.publications.published|dictsort:"paper_nr" %} + <li> + {% include 'partials/journals/publication_card.html' with publication=paper %} + </li> + {% empty %} + <li> + <h3>No publications found for this issue</h3> + </li> + {% endfor %} + </ul> + </div> </div> - </div> - {% endif %} + {% endif %} + + {% if latest_issue %} + <div class="row"> + <div class="col-12"> + <h2 class="highlight-empty text-blue m-0 p-0">Latest issue: Vol. {{ latest_issue.in_volume.number }} issue {{ latest_issue.number }}</h2> + </div> + </div> + <div class="row"> + <div class="col-12"> + <ul class="list-unstyled"> + {% for paper in latest_issue.publications.published|dictsort:"paper_nr" %} + <li> + {% include 'partials/journals/publication_card.html' with publication=paper %} + </li> + {% empty %} + <li> + <h3>No publications found for this issue</h3> + </li> + {% endfor %} + </ul> + </div> + </div> + {% endif %} - {% if latest_issue %} + {% if not current_issue and not latest_issue %} + <div class="row"> + <div class="col-12"> + <h2 class="text-blue">Coming soon!</h2> + </div> + </div> + {% endif %} + {% elif page_object %} <div class="row"> <div class="col-12"> - <h2 class="highlight-empty text-blue m-0 p-0">Latest issue: Vol. {{ latest_issue.in_volume.number }} issue {{ latest_issue.number }}</h2> + <h2 class="text-blue">{{ journal.get_name_display }} Publications</h2> </div> </div> <div class="row"> <div class="col-12"> <ul class="list-unstyled"> - {% for paper in latest_issue.publication_set.all|dictsort:"paper_nr" %} + {% for publication in page_object.object_list %} <li> - {% include 'partials/journals/publication_card.html' with publication=paper %} + {% include 'partials/journals/publication_card.html' with publication=publication %} </li> {% empty %} <li> - <h3>No publications found for this issue</h3> + <h3>First Publication coming soon!</h3> </li> {% endfor %} </ul> </div> </div> - {% endif %} - - {% if not current_issue and not latest_issue %} <div class="row"> <div class="col-12"> - <h2 class="text-blue">Coming soon!</h2> + {% include 'partials/pagination.html' with page_obj=page_object %} </div> </div> + {% else %} + <h3 class="none-found">First Publication coming soon!</h3> {% endif %} {% endblock %} diff --git a/journals/templates/journals/journal_recent.html b/journals/templates/journals/journal_recent.html index d12a180904f8c57ace7aa15c2996930e7e708544..66c96e5019a0a7e72cedac9c31ae546c51953f74 100644 --- a/journals/templates/journals/journal_recent.html +++ b/journals/templates/journals/journal_recent.html @@ -19,7 +19,7 @@ <div class="row"> <div class="col-12"> <ul class="list-unstyled"> - {% for paper in recent_papers %} + {% for paper in journal.get_publications.published|slice:':20' %} <li> {% include 'partials/journals/publication_card.html' with publication=paper %} </li> diff --git a/journals/templates/journals/journals.html b/journals/templates/journals/journals.html index de1e80066fb283e867d99346edbe3e9e2879f199..a696b5cd0184ce4be0100e0afae9a8c1dbcf23cd 100644 --- a/journals/templates/journals/journals.html +++ b/journals/templates/journals/journals.html @@ -77,9 +77,13 @@ </div> </div> <div class="col-md-6"> - <h1 class="banner">SciPost Physics Lecture Notes</h1> + <h1 class="banner"><a href="{% url 'scipost:landing_page' 'SciPostPhysLectNotes' %}">SciPost Physics Lecture Notes</a></h1> <div class="py-2"> - <p>Research-level didactic material in all domains and subject areas of Physics.</p> + <p> + <a href="{% url 'journal:about' 'SciPostPhysLectNotes' %}">Go directly to SciPost Physics Lecture Notes</a> + <br><br> + Research-level didactic material in all domains and subject areas of Physics. + </p> </div> </div> </div> diff --git a/journals/templates/journals/manage_metadata.html b/journals/templates/journals/manage_metadata.html index fc3c61d69d847dd2b23a96a8daaf7722bc7514bb..436a24166152b14180a845659721ebda6d2eb57c 100644 --- a/journals/templates/journals/manage_metadata.html +++ b/journals/templates/journals/manage_metadata.html @@ -19,23 +19,47 @@ event: "focusin" <span class="breadcrumb-item">Manage metadata</span> {% endblock %} +{% block body_class %}{{ block.super }} manage_metadata{% endblock %} + {% block content %} <div class="row"> <div class="col-12"> - <h1 class="highlight">Manage Publications Metadata{% if issue_doi_label %} for issue {{ issue_doi_label }}{% endif %}</h1> - {% if issue_doi_label %} - <h3>Return to the <a href="{% url 'journals:manage_metadata' %}">Main manage metadata page</a></h3> - {% endif %} - <h3>Manage metadata per issue:</h3> - <ul> - {% for issue in issues %} - <li> - <a href="{% url 'journals:manage_metadata' issue_doi_label=issue.doi_label %}">{{ issue }}</a> - </li> - {% endfor %} + <h1 class="highlight">Manage Publications Metadata{% if issue_doi_label %} for issue {{ issue_doi_label }}{% elif journal %} for {{ journal }}{% endif %}</h1> + + </div> +</div> +<div class="row"> + <div class="col-md-6"> + <h3>Available journals</h3> + <ul class="links"> + {% for journal in journals %} + {% url 'journals:manage_metadata' journal_doi_label=journal.doi_label as url %} + <li class="{% if url == request.path %}active{% endif %}"> + <a href="{{ url }}">{{ journal }}</a> + </li> + {% endfor %} </ul> </div> + <div class="col-md-6"> + <h3>Manage metadata per issue:</h3> + {% if journal.has_issues %} + <ul class="links"> + {% for volume in journal.volumes.all %} + {% for issue in volume.issues.all %} + {% url 'journals:manage_metadata' issue_doi_label=journal.doi_label as url %} + <li class="{% if url == request.path %}active{% endif %}"> + <a href="{{ url }}">{{ issue }}</a> + </li> + {% endfor %} + {% empty %} + <li><em>There are no Volumes and Issues for this Journal</em></li> + {% endfor %} + </ul> + {% else %} + <em>This Journal does not have Issues, but individual Publications.</em> + {% endif %} + </div> </div> @@ -53,7 +77,7 @@ event: "focusin" <tbody id="accordion" role="tablist" aria-multiselectable="true"> {% for publication in publications %} - <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ publication.id }}" aria-expanded="true" aria-controls="collapse{{ publication.id }}" style="cursor: pointer;"> + <tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ publication.id }}" aria-expanded="true" aria-controls="collapse{{ publication.id }}" style="cursor: pointer;"{% if not publication.is_published %} class="table-warning"{% endif %}> <td><a href="{{ publication.get_absolute_url }}">{{ publication.doi_label }}</a></td> <td>{{ publication.publication_date }}</td> {% if publication.latest_metadata_update %} @@ -68,22 +92,19 @@ event: "focusin" <td>{{ publication|latest_successful_DOAJ_deposit }}</td> </tr> <tr id="collapse{{ publication.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ publication.id }}" style="background-color: #fff;"> - <td colspan="6"> - - <h2 class="ml-3">Actions</h2> - <div class="row"> - <div class="col-md-5"> + <td colspan="6" class="py-3"> + <div class="row"> + {% if not publication.is_published %} + <div class="col-12"> + <h3 class="text-center bg-warning text-white mb-3">Current status: <em>{{ publication.get_status_display }}</em></h3> + </div> + {% endif %} + + <div class="col-md-6"> + <h2 class="ml-3">Actions</h2> <ul> - <li>Mark the first author - <ul class="list-unstyled pl-4"> - {% for author in publication.authors.all %} - <li> - {{ author.order }}. <a href="{% url 'journals:mark_first_author' doi_label=publication.doi_label author_object_id=author.id %}">{{ author }}</a> - </li> - {% endfor %} - </ul> - </li> <li><a href="{% url 'journals:add_author' doi_label=publication.doi_label %}">Add a missing author</a></li> + <li><a href="{% url 'journals:update_author_ordering' doi_label=publication.doi_label %}">Update Author ordering</a></li> <li><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation list metadata</a></li> <li><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> @@ -93,10 +114,14 @@ event: "focusin" <li><a href="{% url 'journals:produce_metadata_DOAJ' doi_label=publication.doi_label %}">Produce DOAJ metadata</a></li> <li><a href="{% url 'journals:metadata_DOAJ_deposit' doi_label=publication.doi_label %}">Deposit the metadata to DOAJ</a></li> <li><a href="{% url 'journals:harvest_citedby_links' publication.doi_label %}">Update Crossref cited-by links</a></li> + {% if not publication.is_published %} + <li><a href="{% url 'journals:update_publication' publication.accepted_submission.arxiv_identifier_w_vn_nr %}">Update Publication object</a></li> + <li><strong><a href="{% url 'journals:publish_publication' publication.doi_label %}">Publish this Publication</a></strong></li> + {% endif %} </ul> </div> - <div class="col-md-5"> + <div class="col-md-6"> <h2>Funding statement for this publication:</h2> {% if publication.metadata.funding_statement %} <p>{{ publication.metadata.funding_statement }}</p> @@ -116,7 +141,7 @@ event: "focusin" <form action="{% url 'journals:add_associated_grant' publication.doi_label %}" method="post"> {% csrf_token %} {{associate_grant_form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Add"> + <input class="btn btn-outline-secondary" type="submit" value="Add"> </form> <br/> <h2>Generic (not via grant) funders associated to this publication:</h2> @@ -131,7 +156,7 @@ event: "focusin" <form action="{% url 'journals:add_generic_funder' publication.doi_label %}" method="post"> {% csrf_token %} {{associate_generic_funder_form|bootstrap}} - <input class="btn btn-secondary" type="submit" value="Add"> + <input class="btn btn-outline-secondary" type="submit" value="Add"> </form> <br/> <h3>Other funding-related actions:</h3> diff --git a/journals/templates/journals/metadata_xml_deposit.html b/journals/templates/journals/metadata_xml_deposit.html index 960e3737b67c6a3036eb52c7d1d9b177b94967a2..a95c2c9d2c8979034fa4ec9cb586a44e30e52ef3 100644 --- a/journals/templates/journals/metadata_xml_deposit.html +++ b/journals/templates/journals/metadata_xml_deposit.html @@ -34,8 +34,10 @@ <h3 class="mt-3">Response text:</h3> <pre><code>{{ response_text|linebreaks }}</code></pre> - <br> - <p><a href="{{publication.get_absolute_url}}">return to the publication's page</a>, to the <a href="{% url 'journals:manage_metadata' %}">general metadata management page</a> or to <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">this publication's metadata management page</a></p> + {% if perms.scipost.can_publish_accepted_submission %} + <br> + <p><a href="{{publication.get_absolute_url}}">return to the publication's page</a>, to the <a href="{% url 'journals:manage_metadata' %}">general metadata management page</a> or to <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">this publication's metadata management page</a></p> + {% endif %} </div> @@ -48,8 +50,11 @@ You might want to <a href="{% url 'journals:create_metadata_xml' doi_label=publication.doi_label %}">produce new metadata</a> to do a new deposit instead. <p> - <br> - <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">Back to Admin for {{ publication.doi_label }}</a> + {% if perms.scipost.can_publish_accepted_submission %} + <p> + <a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">Back to Admin for {{ publication.doi_label }}</a> + </p> + {% endif %} </div> </div> {% endif %} diff --git a/journals/templates/journals/publication_approval_form.html b/journals/templates/journals/publication_approval_form.html new file mode 100644 index 0000000000000000000000000000000000000000..4daa978337c5de968add6bda355b8327ee3d4499 --- /dev/null +++ b/journals/templates/journals/publication_approval_form.html @@ -0,0 +1,46 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Send Publication for approval{% endblock pagetitle %} + +{% block content %} + + +<h1 class="highlight">Send Publication for approval</h1> +{% include 'partials/journals/publication_summary.html' with publication=form.instance %} + +<h3>Authors</h3> +<ul> + {% for author in form.instance.authors.all %} + <li>{{ author }}</li> + {% empty %} + <li>No authors assigned</li> + {% endfor %} +</ul> + +<h3>Funding statement</h3> +<p>{{ form.instance.metadata.funding_statement|default:'<em>No funding statement found.</em>' }}</p> + +<h3>Grants</h3> +<ul> + {% for grant in form.instance.grants.all %} + <li>{{ grant }}</li> + {% empty %} + <li>No grants assigned</li> + {% endfor %} +</ul> + +{% include 'partials/journals/references.html' with publication=form.instance %} + +<br> +<form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Submit"> +</form> + + + + +{% endblock %} diff --git a/journals/templates/journals/publication_authors_form.html b/journals/templates/journals/publication_authors_form.html new file mode 100644 index 0000000000000000000000000000000000000000..861f712b9e0e4a275ba9b436757658c8352346ac --- /dev/null +++ b/journals/templates/journals/publication_authors_form.html @@ -0,0 +1,47 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Publication Authors{% endblock pagetitle %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + <a href="{% url 'journals:journals' %}" class="breadcrumb-item">Journals</a> + <a href="{{publication.get_absolute_url}}" class="breadcrumb-item">{{publication.citation}}</a> + <span class="breadcrumb-item active">Author ordering</span> + + </nav> + </div> + </div> +{% endblock %} + +{% block content %} + + +<h1 class="highlight">Author Ordering</h1> + +<div class="mb-4"> + {% include 'partials/journals/publication_li_content.html' with publication=publication %} +</div> +<a href="{% url 'journals:add_author' publication.doi_label %}">Add missing author</a> +<h3 class="highlight">Ordering</h3> + +<form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ formset.management_form }} + <ul class="fa-ul sortable-list d-inline-block"> + {% for form in formset %} + <li> + <i class="fa fa-sort"></i> + {{ form.instance.first_name }} {{ form.instance.last_name }} + <div class="d-none">{{ form }}</div> + </li> + {% endfor %} + </ul> + <br> + <input type="submit" class="btn btn-primary" value="Save ordering"> +</form> + +{% endblock %} diff --git a/journals/templates/journals/publication_detail.html b/journals/templates/journals/publication_detail.html index 1522969e8df4a5fd4fd4940b92cd7d863b92509a..c2f1e27c069cdc2a85fc35af99714f296fe7b0d3 100644 --- a/journals/templates/journals/publication_detail.html +++ b/journals/templates/journals/publication_detail.html @@ -1,6 +1,7 @@ {% extends 'journals/_base.html' %} {% load journals_extras %} +{% load publication_administration %} {% load staticfiles %} {% load scipost_extras %} {% load user_groups %} @@ -11,8 +12,10 @@ {% block breadcrumb_items %} {{block.super}} - <a href="{{journal.get_absolute_url}}" class="breadcrumb-item">{{journal}}</a> - <a href="{{publication.in_issue.get_absolute_url}}" class="breadcrumb-item">{{publication.in_issue.short_str}}</a> + <a href="{{ journal.get_absolute_url }}" class="breadcrumb-item">{{ journal }}</a> + {% if publication.in_issue %} + <a href="{{ publication.in_issue.get_absolute_url }}" class="breadcrumb-item">{{ publication.in_issue.short_str }}</a> + {% endif %} <span class="breadcrumb-item active">{{publication.title}}</span> {% endblock %} @@ -30,11 +33,12 @@ <meta name="citation_publication_date" content="{{ publication.publication_date|date:'Y/m/d' }}"/> <meta name="citation_journal_title" content="{{ journal }}"/> <meta name="citation_issn" content="{{ journal.issn }}"/> - <meta name="citation_volume" content="{{ publication.in_issue.in_volume.number }}"/> - <meta name="citation_issue" content="{{ publication.in_issue.number }}"/> + {% if publication.in_issue %} + <meta name="citation_volume" content="{{ publication.in_issue.in_volume.number }}"/> + <meta name="citation_issue" content="{{ publication.in_issue.number }}"/> + {% endif %} <meta name="citation_firstpage" content="{{ publication.paper_nr|paper_nr_string_filter }}"/> <meta name="citation_pdf_url" content="https://scipost.org/{{ publication.doi_string }}/pdf"/> - <meta name="dc.identifier" content="doi:{{ publication.doi_string }}"/> <script> @@ -50,6 +54,17 @@ {% endblock headsup %} {% block content %} + {% if not publication.is_published %} + <div class="card bg-warning text-white"> + <div class="card-body"> + <p class="card-text text-center"> + This Publication is not published yet. + Current status: {{ publication.get_status_display }} + </p> + </div> + </div> + {% endif %} + {% is_edcol_admin request.user as is_edcol_admin %} {% include 'partials/journals/publication_summary.html' with publication=publication %} @@ -127,35 +142,54 @@ </ul> {% endif %} + {% if publication.status == 'draft' and perms.scipost.can_draft_publication %} + <hr class="divider"> + <div class="row"> + <div class="col-12"> + <h3>Publication preparation</h3> + <ul class="fa-ul"> + <li><i class="fa-li fa fa-check-square text-success"></i><a href="{% url 'journals:update_publication' publication.accepted_submission.arxiv_identifier_w_vn_nr %}">Create/update Publication object</a></li> + <li><i class="fa-li fa {% if publication|has_all_author_relations %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:add_author' publication.doi_label %}">Create all author relations</a></li> + <li><i class="fa-li fa {% if publication.has_citation_list %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation metadata</a></li> + <li><i class="fa-li fa {% if publication.has_funding_statement %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> + <li><i class="fa-li fa {% if publication.grants.exists %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:update_grants' publication.doi_label %}">Create/update grants</a></li> + <li><i class="fa-li fa {% if publication.has_xml_metadata %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:create_metadata_xml' publication.doi_label %}">Create/update Crossref metadata</a> <em>(please do after citation and funding info are added)</em></li> + <li><i class="fa-li fa {% if publication.references.exists %}fa-check-square text-success{% else %}fa-square{% endif %}"></i><a href="{% url 'journals:update_references' doi_label=publication.doi_label %}">Create/update references</a></li> + </ul> + + <h3>Tools</h3> + <ul> + <li><a href="{% url 'journals:metadata_xml_deposit' publication.doi_label 'test' %}">Test Crossref deposit</a></li> + </ul> + Preparation completed? <a href="{% url 'journals:send_publication_for_approval' publication.doi_label %}">Send to administration for approval</a>. + </div> + </div> + {% endif %} + {% if is_edcol_admin %} - <hr> - <div class="row"> - <div class="col-12"> - <h3>Editorial Administration tools: </h3> - <ul> - <li> - Mark the first author - <ul class="list-unstyled pl-4"> - {% for author in publication.authors.all %} - <li> - {{ author.order }}. <a href="{% url 'journals:mark_first_author' doi_label=publication.doi_label author_object_id=author.id %}">{{ author }}</a> - </li> - {% endfor %} - </ul> - </li> - <li><a href="{% url 'journals:add_author' doi_label=publication.doi_label %}">Add a missing author</a></li> - <li><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation list metadata</a></li> - <li><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> - <li><a href="{% url 'journals:create_metadata_xml' publication.doi_label %}">Create/update the XML metadata</a></li> - <li><a href="{% url 'journals:metadata_xml_deposit' publication.doi_label 'test' %}">Test metadata deposit (via Crossref test server)</a></li> - <li><a href="{% url 'journals:metadata_xml_deposit' publication.doi_label 'deposit' %}">Deposit the metadata to Crossref</a></li> - <li><a href="{% url 'journals:harvest_citedby_links' publication.doi_label %}">Update Crossref cited-by links</a></li> - <li><a href="{% url 'journals:manage_metadata' %}">Metadata management page</a></li> - <li><a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">This publication's metadata management page</a></li> - <li><a href="{% url 'journals:update_references' doi_label=publication.doi_label %}">Update references</a></li> - </ul> + <hr class="divider"> + <div class="row"> + <div class="col-12"> + <h3>Editorial Administration tools</h3> + <ul class="mb-0"> + <li><a href="{% url 'journals:add_author' doi_label=publication.doi_label %}">Add a missing author</a></li> + <li><a href="{% url 'journals:update_author_ordering' doi_label=publication.doi_label %}">Update Author ordering</a></li> + <li><a href="{% url 'journals:create_citation_list_metadata' publication.doi_label %}">Create/update citation list metadata</a></li> + <li><a href="{% url 'journals:create_funding_info_metadata' publication.doi_label %}">Create/update funding info metadata</a></li> + <li><a href="{% url 'journals:create_metadata_xml' publication.doi_label %}">Create/update the XML metadata</a></li> + <li><a href="{% url 'journals:metadata_xml_deposit' publication.doi_label 'test' %}">Test metadata deposit (via Crossref test server)</a></li> + <li><a href="{% url 'journals:metadata_xml_deposit' publication.doi_label 'deposit' %}">Deposit the metadata to Crossref</a></li> + <li><a href="{% url 'journals:harvest_citedby_links' publication.doi_label %}">Update Crossref cited-by links</a></li> + <li><a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">Metadata management page</a></li> + <li><a href="{% url 'journals:manage_metadata' doi_label=publication.doi_label %}">This publication's metadata management page</a></li> + <li><a href="{% url 'journals:update_references' doi_label=publication.doi_label %}">Update references</a></li> + {% if not publication.is_published %} + <li><a href="{% url 'journals:update_publication' publication.accepted_submission.arxiv_identifier_w_vn_nr %}">Update Publication object</a></li> + <li><strong><a href="{% url 'journals:publish_publication' publication.doi_label %}">Publish this Publication</a></strong></li> + {% endif %} + </ul> + </div> </div> - </div> {% endif %} {% endblock content %} diff --git a/journals/templates/journals/publication_form.html b/journals/templates/journals/publication_form.html new file mode 100644 index 0000000000000000000000000000000000000000..8d46616c3d1fbc955e347d6034a773420a9ec485 --- /dev/null +++ b/journals/templates/journals/publication_form.html @@ -0,0 +1,32 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Draft publication{% endblock pagetitle %} + +{% block content %} + + +<h1>Draft publication</h1> + +{% if request.GET.issue or form.instance.id or form.to_journal %} + <form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Save"> + </form> +{% else %} + <h3>Pick the Issue to publish in</h3> + <ul> + {% for issue in form.get_possible_issues %} + <li><a href="?issue={{ issue.id }}">{{ issue }}</a></li> + {% empty %} + <li><em>No Issues found. Please contact administration</em></li> + {% endfor %} + </ul> +{% endif %} + + + + +{% endblock %} diff --git a/journals/templates/journals/publication_list.html b/journals/templates/journals/publication_list.html new file mode 100644 index 0000000000000000000000000000000000000000..a02ac4f2e75c746dd71471050d95f8ab82d28509 --- /dev/null +++ b/journals/templates/journals/publication_list.html @@ -0,0 +1,65 @@ +{% extends 'scipost/layout_2_col.html' %} + +{% load bootstrap %} +{% load request_filters %} +{% load submissions_extras %} +{% load request_filters %} + +{% block body_class %}{{block.super}} sidebar-left{% endblock %} + +{% block pagetitle %}: Publications{% endblock pagetitle %} + +{% block page_header %}<h1 class="highlight">Recent SciPost Publications</h1>{% endblock page_header %} + +{% block breadcrumb_items %} + <a href="{% url 'journals:journals' %}" class="breadcrumb-item">Journals</a> + <span class="breadcrumb-item">Publications</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <ul class="list-group list-group-flush"> + {% for publication in object_list %} + <li class="list-group-item"> + <div class="card-body px-0"> + {% include 'partials/journals/publication_li_content_extended.html' with publication=publication %} + </div> + </li> + {% empty %} + <h3><em>No match found for your search query.</em></h3> + {% endfor %} + </ul> + </div> + {% if is_paginated %} + <div class="col-12"> + {% include 'partials/pagination.html' with page_obj=page_obj %} + </div> + {% endif %} +</div> + +{% endblock content %} + + +{% block sidebar %} + <div class="row"> + <div class="col-12"> + <h2 class="mb-2">Recent Issues</h2> + <div class="mb-1 pl-2"> + <a href="?{% url_replace issue='' page='' %}" class="{% active_get_request 'issue' '' %}">All Issues</a> + </div> + {% for issue in recent_issues %} + <div class="mb-1 pl-2"> + <a href="?{% url_replace issue=issue.id page='' %}" class="{% active_get_request 'issue' issue.id %}">{{ issue.in_volume.in_journal }} {{ issue.short_str }}</a> + <br>{{ issue.period_as_string }} + </div> + {% endfor %} + + <h2 class="mb-2 mt-4">Subject area</h2> + {% for subject in subject_areas %} + <a href="?{% url_replace subject=subject.0 page='' %}" class="d-inline-block mb-1 ml-2 {% active_get_request 'subject' subject.0 %}">{{ subject.1 }}</a><br> + {% endfor %} + </div> + </div> +{% endblock %} diff --git a/journals/templates/journals/publication_publish_form.html b/journals/templates/journals/publication_publish_form.html new file mode 100644 index 0000000000000000000000000000000000000000..b361b0c290a7bf02204221da22a49548b99e4d4d --- /dev/null +++ b/journals/templates/journals/publication_publish_form.html @@ -0,0 +1,57 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Publish Publication{% endblock pagetitle %} + +{% block content %} + + +<h1 class="highlight">Publish Publication</h1> +{% include 'partials/journals/publication_summary.html' with publication=form.instance %} + +<h3>Authors</h3> +<ul> + {% for author in form.instance.authors.all %} + <li>{{ author }}</li> + {% empty %} + <li>No authors assigned</li> + {% endfor %} +</ul> + +<h3>Funding statement</h3> +<p>{{ form.instance.metadata.funding_statement|default:'<em>No funding statement found.</em>' }}</p> + +<h3>Grants</h3> +<ul> + {% for grant in form.instance.grants.all %} + <li>{{ grant }}</li> + {% empty %} + <li>No grants assigned</li> + {% endfor %} +</ul> + +{% include 'partials/journals/references.html' with publication=form.instance %} + +<hr class="divider"> +<h3>Publishing will do the following:</h3> +<div> + <ul> + <li>Move the pdf file to the appropriate folder</li> + <li>Update the Submission status</li> + <li>Update the Production Stream status</li> + <li>Send the authors a publication-email</li> + </ul> + <em>Reminder: Is the metadata already deposited at Crossref?</em> +</div> +<br> +<form method="post" enctype="multipart/form-data"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Publish"> +</form> + + + + +{% endblock %} diff --git a/journals/templates/journals/sign_existing_report.html b/journals/templates/journals/sign_existing_report.html index 95d198d9a267399052d7b459f7044a1037410bb5..4e2649a0b7884e6d8c5d9904089ff039b5bbcd35 100644 --- a/journals/templates/journals/sign_existing_report.html +++ b/journals/templates/journals/sign_existing_report.html @@ -34,7 +34,7 @@ <form action="{% url 'journals:sign_existing_report' report_id=report.id %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> </div> </div> diff --git a/journals/templates/journals/validate_publication.html b/journals/templates/journals/validate_publication.html deleted file mode 100644 index db78a13e090435434f2826e8dee528feed773071..0000000000000000000000000000000000000000 --- a/journals/templates/journals/validate_publication.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'scipost/base.html' %} - -{% load bootstrap %} - -{% block pagetitle %}: Validate publication{% endblock pagetitle %} - -{% block content %} - -<div class="row"> - <div class="col-12"> - <h1 class="highlight">Validate publication page</h1> - </div> -</div> - -<div class="row justify-content-center"> - <div class="col-lg-10"> - {% if errormessage %} - <h2 style="color: red;">{{ errormessage }}</h2> - {% endif %} - - <form action="{% url 'journals:validate_publication' %}" method="post" enctype="multipart/form-data"> - {% csrf_token %} - {{ validate_publication_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"> - </form> - </div> -</div> - -{% endblock content %} diff --git a/journals/templates/partials/journals/publication_li_content.html b/journals/templates/partials/journals/publication_li_content.html index 2f8346ba21f8b2516f3cc1698d256f787030363d..9bb1406455a4bc4b18b6c3516416460ce667c9e3 100644 --- a/journals/templates/partials/journals/publication_li_content.html +++ b/journals/templates/partials/journals/publication_li_content.html @@ -1,7 +1,9 @@ -<h5 class="pb-0">{{publication.get_subject_area_display}}</h5> -<h3><a href="{{publication.get_absolute_url}}">{{publication.title}}</a></h3> +<div class="li publication"> + <h5 class="subject">{{publication.get_subject_area_display}}</h5> + <h3 class="title"><a href="{{publication.get_absolute_url}}">{{publication.title}}</a></h3> -<p class="mt-0 mb-2">{{ publication.author_list }}</p> -<p class="text-muted mb-0"> - {{ publication.citation }} · <span class="font-weight-light">published {{ publication.publication_date|date:'j F Y' }}</span> -</p> + <p class="authors">{{ publication.author_list }}</p> + <p class="meta mb-0"> + {{ publication.citation }} · <span class="font-weight-light">published {{ publication.publication_date|date:'j F Y' }}</span> + </p> +</div> diff --git a/journals/templates/partials/journals/publication_li_content_extended.html b/journals/templates/partials/journals/publication_li_content_extended.html new file mode 100644 index 0000000000000000000000000000000000000000..400d677b974f58134632d7980a860b0128da5139 --- /dev/null +++ b/journals/templates/partials/journals/publication_li_content_extended.html @@ -0,0 +1,17 @@ +<div class="li publication"> + <h5 class="subject">{{ publication.citation }}</h5> + <h3 class="title"><a href="{{publication.get_absolute_url}}">{{publication.title}}</a></h3> + + <p class="authors">{{ publication.author_list }}</p> + <p class="meta"> + doi: <a href="{{ publication.get_absolute_url }}">{{ publication.doi_string }}</a> · + <span class="font-weight-light">published {{ publication.publication_date|date:'j F Y' }}</span> · + <a href="{% url 'scipost:publication_pdf' publication.doi_label %}" target="_blank"><i class="fa fa-file-pdf-o" aria-hidden="true"></i> pdf</a> + </p> + <div class="abstract"> + <a href="javascript:;" data-toggle="toggle" data-target="#abstract-{{ publication.id }}">+ Show abstract</a> + <div id="abstract-{{ publication.id }}" style="display: none;" class="mt-2"> + {{ publication.abstract }} + </div> + </div> +</div> diff --git a/journals/templates/partials/journals/publication_ris.html b/journals/templates/partials/journals/publication_ris.html new file mode 100644 index 0000000000000000000000000000000000000000..64d1bf2cf6f3df71a0bfbf06c9f0a8eebc79f5ab --- /dev/null +++ b/journals/templates/partials/journals/publication_ris.html @@ -0,0 +1,28 @@ +{% spaceless %} + <pre> + <code> + <span>TY - JOUR</span><br> + <span>PB - SciPost Foundation</span><br> + <span>DO - {{ publication.doi_string }}</span><br> + <span>TI - {{ publication.title }}</span><br> + <span>PY - {{ publication.publication_date|date:'Y/m/d' }}</span><br> + <span>UR - https://scipost.org{{ publication.get_absolute_url }}</span><br> + <span>JF - {{ publication.get_journal.get_name_display }}</span><br> + <span>JA - {{ publication.get_journal.abbreviation_citation }}</span><br> + {% if publication.in_issue %} + <span>VL - {{ publication.in_issue.in_volume.number }}</span><br> + <span>IS - {{ publication.in_issue.number }}</span><br> + {% endif %} + <span>SP - {{ publication.get_paper_nr }}</span><br> + {% for author in publication.authors.all %} + {% if forloop.first %} + <span>A1 - {{ author.last_name }}, {{ author.first_name }}</span><br> + {% else %} + <span>AU - {{ author.last_name }}, {{ author.first_name }}</span><br> + {% endif %} + {% endfor %} + <span>AB - {{ publication.abstract }}</span><br> + <span>ER -</span> + </code> + </pre> +{% endspaceless %} diff --git a/journals/templates/partials/journals/publication_summary.html b/journals/templates/partials/journals/publication_summary.html index 014da1f23f015d0a82d9b59bf0be546a35470a09..ff6b434747ef4d4944b263e1dc90df7a49af6d24 100644 --- a/journals/templates/partials/journals/publication_summary.html +++ b/journals/templates/partials/journals/publication_summary.html @@ -1,7 +1,7 @@ <div class="row"> <div class="col-12"> - <h2 class="pb-1 text-blue">{{publication.title}}</h2> + <h2 class="pb-1 text-blue">{{publication.title}}{% if publication.status == 'draft' %} <label class="label label-warning label-sm">{{ publication.get_status_display }}</label>{% endif %}</h2> <p class="mb-1">{{ publication.author_list }}</p> <p class="text-muted mb-0"> @@ -17,6 +17,7 @@ <a href="{{publication.get_absolute_url}}/pdf" target="_blank">pdf</a> </li> <li><a href="javascript:;" data-toggle="modal" data-target="#bibtextmodal">BiBTeX</a></li> + <li><a href="javascript:;" data-toggle="modal" data-target="#rismodal">RIS</a></li> <li><a href="{% url 'submissions:submission' publication.accepted_submission.arxiv_identifier_w_vn_nr %}">Submissions/Reports</a></li> <li> <!-- Start Crossmark Snippet v2.0 --> @@ -40,6 +41,22 @@ </div> +<div class="modal" id="rismodal" tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="rismodal"> + <div class="modal-dialog modal-lg" role="document"> + <div class="modal-content"> + <div class="modal-body"> + <div class="pb-4"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div>{% include 'partials/journals/publication_ris.html' with publication=publication %}</div> + </div> + </div> + </div> +</div> + + <div class="modal" id="bibtextmodal" tabindex="-1" role="dialog" aria-hidden="true" aria-labelledby="bibtextmodal"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> diff --git a/journals/templates/xml/publication_crossref.html b/journals/templates/xml/publication_crossref.html new file mode 100644 index 0000000000000000000000000000000000000000..f4fb350348d65e592d805133ea4abad061bffaa8 --- /dev/null +++ b/journals/templates/xml/publication_crossref.html @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="UTF-8"?> +<doi_batch version="4.4.0" xmlns="http://www.crossref.org/schema/4.4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fr="http://www.crossref.org/fundref.xsd" xsi:schemaLocation="http://www.crossref.org/schema/4.4.0 http://www.crossref.org/shema/deposit/crossref4.4.0.xsd" xmlns:ai="http://www.crossref.org/AccessIndicators.xsd"> + <head> + <doi_batch_id>{{ doi_batch_id }}</doi_batch_id> + <timestamp>{% now "YmdHis" %}</timestamp> + <depositor> + <depositor_name>scipost</depositor_name> + <email_address>{{ deposit_email }}</email_address> + </depositor> + <registrant>scipost</registrant> + </head> + <body> + <journal> + {% if publication.in_issue %} + <journal_metadata> + <full_title>{{ publication.in_issue.in_volume.in_journal.get_name_display }}</full_title> + <abbrev_title>{{ publication.in_issue.in_volume.in_journal.abbreviation_citation }}</abbrev_title> + <issn media_type='electronic'>{{ publication.in_issue.in_volume.in_journal.issn }}</issn> + <doi_data> + <doi>{{ publication.in_issue.in_volume.in_journal.doi_string }}</doi> + <resource>https://scipost.org/{{ publication.in_issue.in_volume.in_journal.doi_string }}</resource> + </doi_data> + </journal_metadata> + <journal_issue> + <publication_date media_type='online'> + <year>{{ publication.publication_date|date:'Y' }}</year> + </publication_date> + <journal_volume> + <volume>{{ publication.in_issue.in_volume.number }}</volume> + </journal_volume> + <issue>{{ publication.in_issue.number }}</issue> + </journal_issue> + {% else %} + <journal_metadata> + <full_title>{{ publication.in_journal.get_name_display }}</full_title> + <abbrev_title>{{ publication.in_journal.abbreviation_citation }}</abbrev_title> + <issn media_type='electronic'>{{ publication.in_journal.issn }}</issn> + <doi_data> + <doi>{{ publication.in_journal.doi_string }}</doi> + <resource>https://scipost.org/{{ publication.in_journal.doi_string }}</resource> + </doi_data> + </journal_metadata> + {% endif %} + <journal_article publication_type='full_text'> + <titles> + <title>{{ publication.title }}</title> + </titles> + + <contributors> + {% for author_object in publication.authors.all %} + {% if author_object.order == 1 %} + <person_name sequence='first' contributor_role='author'> + {% else %} + <person_name sequence='additional' contributor_role='author'> + {% endif %} + <given_name>{{ author_object.first_name }}</given_name> + <surname>{{ author_object.last_name }}</surname> + {% if author_object.contributor and author_object.contributor.orcid_id %} + <ORCID>http://orcid.org/'{{ author_object.contributor.orcid_id }}</ORCID> + {% endif %} + </person_name> + {% endfor %} + </contributors> + + <publication_date media_type='online'> + <month>{{ publication.publication_date|date:'m' }}</month> + <day>{{ publication.publication_date|date:'d' }}</day> + <year>{{ publication.publication_date|date:'Y' }}</year> + </publication_date> + <publisher_item> + <item_number item_number_type='article_number'>{{ publication.paper_nr }}</item_number> + </publisher_item> + <crossmark> + <crossmark_policy>10.21468/SciPost.CrossmarkPolicy</crossmark_policy> + <crossmark_domains> + <crossmark_domain><domain>scipost.org</domain></crossmark_domain> + </crossmark_domains> + <crossmark_domain_exclusive>false</crossmark_domain_exclusive> + <custom_metadata> + {% if funders %} + <fr:program name='fundref'> + {% for funder in funders %} + {% if funders|length > 1 %} + <fr:assertion name='fundgroup'> + {% endif %} + + <fr:assertion name='funder_name'>{{ funder.name }} + <fr:assertion name='funder_identifier'>{{ funder.identifier }}</fr:assertion> + </fr:assertion> + + {% for grant in publication.grants.all %} + {% if grant.funder == funder %} + <fr:assertion name='award_number'>{{ grant.number }}</fr:assertion> + {% endif %} + {% endfor %} + + {% if funders|length > 1 %} + </fr:assertion> + {% endif %} + {% endfor %} + </fr:program> + {% endif %} + + <ai:program name="AccessIndicators"> + <ai:license_ref>{{ publication.get_cc_license_URI }}</ai:license_ref> + </ai:program> + </custom_metadata> + </crossmark> + <archive_locations> + <archive name="CLOCKSS"></archive> + </archive_locations> + <doi_data> + <doi>{{ publication.doi_string }}</doi> + <resource>https://scipost.org/{{ publication.doi_string }}</resource> + <collection property='crawler-based'> + <item crawler='iParadigms'> + <resource>https://scipost.org/{{ publication.doi_string }}/pdf</resource> + </item> + </collection> + <collection property='text-mining'> + <item> + <resource mime_type='application/pdf'>https://scipost.org/{{ publication.doi_string }}/pdf</resource> + </item> + </collection> + </doi_data> + {% if publication.metadata.citation_list %} + <citation_list> + {% for ref in publication.metadata.citation_list %} + <citation key='{{ ref.key }}'> + <doi>{{ ref.doi }}</doi> + </citation> + {% endfor %} + </citation_list> + {% endif %} + </journal_article> + </journal> + </body> +</doi_batch> diff --git a/journals/templatetags/journals_extras.py b/journals/templatetags/journals_extras.py index fa6370f73bf45bfcb683840a82f28d5767d9e93d..ee55822049fe25ea07c7e8408e4a1af39154bb08 100644 --- a/journals/templatetags/journals_extras.py +++ b/journals/templatetags/journals_extras.py @@ -9,6 +9,7 @@ register = template.Library() def paper_nr_string_filter(nr): return paper_nr_string(nr) + @register.filter(name='latest_successful_crossref_deposit') def latest_successful_crossref_deposit(publication): latest = publication.deposit_set.filter( @@ -18,6 +19,7 @@ def latest_successful_crossref_deposit(publication): else: return "No successful deposit found" + @register.filter(name='latest_successful_DOAJ_deposit') def latest_successful_DOAJ_deposit(publication): latest = publication.doajdeposit_set.filter( @@ -27,8 +29,9 @@ def latest_successful_DOAJ_deposit(publication): else: return "No successful deposit found" + @register.filter(name='latest_successful_crossref_deposit_report') -def latest_successful_crossref_deposit(report): +def latest_successful_crossref_deposit_report(report): latest = report.genericdoideposit.filter( deposit_successful=True).order_by('-deposition_date').first() if latest: @@ -36,8 +39,9 @@ def latest_successful_crossref_deposit(report): else: return "No successful deposit found" + @register.filter(name='latest_successful_crossref_deposit_comment') -def latest_successful_crossref_deposit(comment): +def latest_successful_crossref_deposit_comment(comment): latest = comment.genericdoideposit.filter( deposit_successful=True).order_by('-deposition_date').first() if latest: diff --git a/journals/templatetags/publication_administration.py b/journals/templatetags/publication_administration.py new file mode 100644 index 0000000000000000000000000000000000000000..96e108fbd1f9e9a68e0d0bc3557d1d340b776d20 --- /dev/null +++ b/journals/templatetags/publication_administration.py @@ -0,0 +1,11 @@ +from django import template + +register = template.Library() + + +@register.filter +def has_all_author_relations(publication): + """ + Check if all authors are added to the Publication object, just by counting. + """ + return len(publication.author_list.split(',')) == publication.authors.count() diff --git a/journals/urls/general.py b/journals/urls/general.py index ca60342a93f2fb64dbd834e5fbe14fb8a7f44b56..7d4430e1b5def101440e9dc01e32a73490959222 100644 --- a/journals/urls/general.py +++ b/journals/urls/general.py @@ -2,11 +2,16 @@ from django.conf.urls import url from django.urls import reverse_lazy from django.views.generic import TemplateView, RedirectView +from submissions.constants import SUBMISSIONS_COMPLETE_REGEX + +from journals.constants import PUBLICATION_DOI_REGEX, REGEX_CHOICES from journals import views as journals_views + urlpatterns = [ # Journals url(r'^$', journals_views.journals, name='journals'), + url(r'^publications$', journals_views.PublicationListView.as_view(), name='publications'), url(r'scipost_physics', RedirectView.as_view(url=reverse_lazy('scipost:landing_page', args=['SciPostPhys']))), url(r'^journals_terms_and_conditions$', @@ -16,57 +21,80 @@ urlpatterns = [ TemplateView.as_view(template_name='journals/crossmark_policy.html'), name='crossmark_policy'), + # Publication creation + url(r'^admin/publications/{regex}/$'.format(regex=SUBMISSIONS_COMPLETE_REGEX), + journals_views.DraftPublicationUpdateView.as_view(), + name='update_publication'), + url(r'^admin/publications/(?P<doi_label>{regex})/publish$'.format(regex=PUBLICATION_DOI_REGEX), + journals_views.PublicationPublishView.as_view(), + name='publish_publication'), + url(r'^admin/publications/(?P<doi_label>{regex})/approval$'.format( + regex=PUBLICATION_DOI_REGEX), + journals_views.DraftPublicationApprovalView.as_view(), + name='send_publication_for_approval'), + url(r'^admin/publications/(?P<doi_label>{regex})/authors$'.format(regex=PUBLICATION_DOI_REGEX), + # journals_views.PublicationAuthorOrderingView.as_view(), + journals_views.publication_authors_ordering, + name='update_author_ordering'), + url(r'^admin/publications/(?P<doi_label>{regex})/grants$'.format(regex=PUBLICATION_DOI_REGEX), + journals_views.PublicationGrantsView.as_view(), + name='update_grants'), + url(r'^admin/publications/(?P<doi_label>{regex})/grants/(?P<grant_id>[0-9]+)/remove$'.format( + regex=PUBLICATION_DOI_REGEX), + journals_views.PublicationGrantsRemovalView.as_view(), + name='remove_grant'), + # Editorial and Administrative Workflow - url(r'^admin/initiate_publication$', - journals_views.initiate_publication, - name='initiate_publication'), - url(r'^admin/validate_publication$', - journals_views.validate_publication, - name='validate_publication'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/authors/add/(?P<contributor_id>[0-9]+)$', + url(r'^admin/(?P<doi_label>{regex})/authors/add/(?P<contributor_id>[0-9]+)$'.format( + regex=PUBLICATION_DOI_REGEX), journals_views.add_author, name='add_author'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/authors/add$', + url(r'^admin/(?P<doi_label>{regex})/authors/add$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.add_author, name='add_author'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/authors/mark_first/(?P<author_object_id>[0-9]+)$', - journals_views.mark_first_author, - name='mark_first_author'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/manage_metadata$', + url(r'^admin/(?P<doi_label>{regex})/manage_metadata$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.manage_metadata, name='manage_metadata'), url(r'^admin/(?P<issue_doi_label>[a-zA-Z]+.[0-9]+.[0-9]+)/manage_metadata$', journals_views.manage_metadata, name='manage_metadata'), + url(r'^admin/(?P<journal_doi_label>{regex})/manage_metadata$'.format(regex=REGEX_CHOICES), + journals_views.manage_metadata, + name='manage_metadata'), url(r'^admin/manage_metadata/$', journals_views.manage_metadata, name='manage_metadata'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/citation_list_metadata$', - journals_views.create_citation_list_metadata, + url(r'^admin/(?P<doi_label>{regex})/citation_list_metadata$'.format( + regex=PUBLICATION_DOI_REGEX), + journals_views.CitationUpdateView.as_view(), name='create_citation_list_metadata'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/update_references$', + url(r'^admin/(?P<doi_label>{regex})/update_references$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.update_references, name='update_references'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/funders/create_metadata$', - journals_views.create_funding_info_metadata, + url(r'^admin/(?P<doi_label>{regex})/funders/create_metadata$'.format( + regex=PUBLICATION_DOI_REGEX), + journals_views.FundingInfoView.as_view(), name='create_funding_info_metadata'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/funders/add_generic$', + url(r'^admin/(?P<doi_label>{regex})/funders/add_generic$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.add_generic_funder, name='add_generic_funder'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/grants/add$', + url(r'^admin/(?P<doi_label>{regex})/grants/add$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.add_associated_grant, name='add_associated_grant'), # Metadata handling - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/metadata/crossref/create$', - journals_views.create_metadata_xml, + url(r'^admin/(?P<doi_label>{regex})/metadata/crossref/create$'.format( + regex=PUBLICATION_DOI_REGEX), + journals_views.CreateMetadataXMLView.as_view(), name='create_metadata_xml'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/metadata/crossref/deposit/(?P<option>[a-z]+)$', + url(r'^admin/(?P<doi_label>{regex})/metadata/crossref/deposit/(?P<option>[a-z]+)$'.format( + regex=PUBLICATION_DOI_REGEX), journals_views.metadata_xml_deposit, name='metadata_xml_deposit'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/metadata/DOAJ$', + url(r'^admin/(?P<doi_label>{regex})/metadata/DOAJ$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.produce_metadata_DOAJ, name='produce_metadata_DOAJ'), - url(r'^admin/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/metadata/DOAJ/deposit$', + url(r'^admin/(?P<doi_label>{regex})/metadata/DOAJ/deposit$'.format( + regex=PUBLICATION_DOI_REGEX), journals_views.metadata_DOAJ_deposit, name='metadata_DOAJ_deposit'), url(r'^admin/metadata/crossref/(?P<deposit_id>[0-9]+)/mark/(?P<success>[0-1])$', @@ -89,7 +117,7 @@ urlpatterns = [ url(r'^admin/citedby/$', journals_views.harvest_citedby_list, name='harvest_citedby_list'), - url(r'^admin/citedby/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/harvest$', + url(r'^admin/citedby/(?P<doi_label>{regex})/harvest$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.harvest_citedby_links, name='harvest_citedby_links'), diff --git a/journals/urls/journal.py b/journals/urls/journal.py index 5cffb7d42d425137fe5e89fe136f45fa2e71472b..e8526ee4dfef73b5f1a49b7f14aea97fe859be9b 100644 --- a/journals/urls/journal.py +++ b/journals/urls/journal.py @@ -4,9 +4,9 @@ from journals import views as journals_views urlpatterns = [ # Journal routes - url(r'^issues$', journals_views.issues, name='issues'), - url(r'^recent$', journals_views.recent, name='recent'), - url(r'^accepted$', journals_views.accepted, name='accepted'), + url(r'^issues$', journals_views.IssuesView.as_view(), name='issues'), + url(r'^recent$', journals_views.RecentView.as_view(), name='recent'), + url(r'^accepted$', journals_views.AcceptedView.as_view(), name='accepted'), url(r'^info_for_authors$', journals_views.info_for_authors, name='info_for_authors'), url(r'^about$', journals_views.about, name='about'), ] diff --git a/journals/utils.py b/journals/utils.py index 9ae86f9461d9c4784b9f8f2e16e89b524e7212c7..c5788610701b1f31b61c83a6a46554adf2fdab7d 100644 --- a/journals/utils.py +++ b/journals/utils.py @@ -18,7 +18,7 @@ class JournalUtils(BaseMailUtil): cls.publication.accepted_submission.title + ' by ' + cls.publication.accepted_submission.author_list + '\n\nhas been published online with reference ' - + cls.publication.citation() + '.' + + cls.publication.citation + '.' '\n\nThe publication page is located at the permanent link ' 'https://scipost.org/' + cls.publication.doi_label + '.' '\n\nThe permanent DOI for your publication is 10.21468/' diff --git a/journals/views.py b/journals/views.py index a9a657e354cc741b12393e9b604981bc2cc2140d..f4a338d5d836be9a6ae9f748dd209bb1b818c5fc 100644 --- a/journals/views.py +++ b/journals/views.py @@ -10,6 +10,7 @@ import xml.etree.ElementTree as ET from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse from django.conf import settings @@ -17,27 +18,31 @@ from django.contrib import messages from django.db import transaction from django.http import Http404, HttpResponse from django.utils import timezone -from django.shortcuts import get_object_or_404, render, redirect +from django.utils.decorators import method_decorator +from django.views.generic.detail import DetailView +from django.views.generic.edit import UpdateView +from django.views.generic.list import ListView +from django.shortcuts import get_object_or_404, get_list_or_404, render, redirect -from .exceptions import PaperNumberingError -from .helpers import paper_nr_string, issue_doi_label_from_doi_label +from .constants import STATUS_DRAFT from .models import Journal, Issue, Publication, Deposit, DOAJDeposit,\ GenericDOIDeposit, PublicationAuthorsTable -from .forms import FundingInfoForm, InitiatePublicationForm, ValidatePublicationForm,\ +from .forms import FundingInfoForm,\ UnregisteredAuthorForm, CreateMetadataXMLForm, CitationListBibitemsForm,\ - ReferenceFormSet, CreateMetadataDOAJForm + ReferenceFormSet, CreateMetadataDOAJForm, DraftPublicationForm,\ + PublicationGrantsForm, DraftPublicationApprovalForm, PublicationPublishForm,\ + PublicationAuthorOrderingFormSet +from .mixins import PublicationMixin, ProdSupervisorPublicationPermissionMixin from .utils import JournalUtils from comments.models import Comment -from funders.models import Funder -from submissions.models import Submission, Report -from scipost.models import Contributor -from production.constants import PROOFS_PUBLISHED -from production.models import ProductionEvent -from production.signals import notify_stream_status_change - from funders.forms import FunderSelectForm, GrantSelectForm +from funders.models import Grant +from submissions.models import Submission, Report +from scipost.constants import SCIPOST_SUBJECT_AREAS from scipost.forms import ConfirmationForm +from scipost.models import Contributor +from scipost.mixins import PermissionsMixin, RequestViewMixin, PaginationMixin from guardian.decorators import permission_required @@ -52,101 +57,123 @@ def journals(request): return render(request, 'journals/journals.html', context) -def landing_page(request, doi_label): - journal = get_object_or_404(Journal, doi_label=doi_label) +class PublicationListView(PaginationMixin, ListView): + """ + Show Publications filtered per subject area. + """ + queryset = Publication.objects.published() + paginate_by = 10 - current_issue = Issue.objects.published( - in_volume__in_journal=journal, - start_date__lte=timezone.now(), - until_date__gte=timezone.now()).order_by('-until_date').first() - latest_issue = Issue.objects.published( - in_volume__in_journal=journal, - until_date__lte=timezone.now()).order_by('-until_date').first() + def get_queryset(self): + qs = super().get_queryset() + if self.request.GET.get('issue'): + qs = qs.filter(in_issue__id=int(self.request.GET['issue'])) + if self.request.GET.get('subject'): + qs = qs.for_subject(self.request.GET['subject']) + return qs.order_by('-publication_date') - prev_issue = None - if current_issue: - prev_issue = (Issue.objects.published(in_volume__in_journal=journal, - start_date__lt=current_issue.start_date) - .order_by('start_date').last()) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['recent_issues'] = Issue.objects.published().order_by('-start_date')[:5] + context['subject_areas'] = (('', 'Show all'),) + SCIPOST_SUBJECT_AREAS[0][1] + return context + +def landing_page(request, doi_label): + """ + The landing page of a Journal lists either the latest and the current issue of a Journal + of paginates the individual Publications. + """ + journal = get_object_or_404(Journal, doi_label=doi_label) context = { - 'current_issue': current_issue, - 'latest_issue': latest_issue, - 'prev_issue': prev_issue, 'journal': journal } - return render(request, 'journals/journal_landing_page.html', context) + if journal.has_issues: + current_issue = Issue.objects.published().filter( + in_volume__in_journal=journal, + start_date__lte=timezone.now(), + until_date__gte=timezone.now()).first() + latest_issue = Issue.objects.published().filter( + in_volume__in_journal=journal, + until_date__lte=timezone.now()).first() + prev_issue = None + if current_issue: + prev_issue = Issue.objects.published().filter( + in_volume__in_journal=journal, start_date__lt=current_issue.start_date + ).order_by('start_date').last() + + context.update({ + 'current_issue': current_issue, + 'latest_issue': latest_issue, + 'prev_issue': prev_issue, + }) + else: + paginator = Paginator(journal.publications.published(), 10) + context.update({ + 'page_object': paginator.page(request.GET.get('page', 1)), + }) + return render(request, 'journals/journal_landing_page.html', context) -def issues(request, doi_label): - journal = get_object_or_404(Journal, doi_label=doi_label) - issues = Issue.objects.published(in_volume__in_journal=journal).order_by('-until_date') - context = { - 'issues': issues, - 'journal': journal - } - return render(request, 'journals/journal_issues.html', context) +class IssuesView(DetailView): + """ + List all Issues sorted per Journal. + """ + queryset = Journal.objects.has_issues() + slug_field = slug_url_kwarg = 'doi_label' + template_name = 'journals/journal_issues.html' -def recent(request, doi_label): +class RecentView(DetailView): """ - Display page for the most recent 20 publications in SciPost Physics. + List all recent Publications for a specific Journal. """ - journal = get_object_or_404(Journal, doi_label=doi_label) - recent_papers = Publication.objects.published( - in_issue__in_volume__in_journal=journal).order_by('-publication_date', - '-paper_nr')[:20] - context = { - 'recent_papers': recent_papers, - 'journal': journal, - } - return render(request, 'journals/journal_recent.html', context) + queryset = Journal.objects.active() + slug_field = slug_url_kwarg = 'doi_label' + template_name = 'journals/journal_recent.html' -def accepted(request, doi_label): +class AcceptedView(DetailView): """ - Display page for submissions to SciPost Physics which - have been accepted but are not yet published. + List all Submissions for a specific Journal which have been accepted but are not + yet published. """ - journal = get_object_or_404(Journal, doi_label=doi_label) - accepted_SP_submissions = (Submission.objects.accepted() - .filter(submitted_to_journal=journal.name) - .order_by('-latest_activity')) - context = { - 'accepted_SP_submissions': accepted_SP_submissions, - 'journal': journal - } - return render(request, 'journals/journal_accepted.html', context) + queryset = Journal.objects.active() + slug_field = slug_url_kwarg = 'doi_label' + template_name = 'journals/journal_accepted.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['accepted_submissions'] = Submission.objects.accepted().filter( + submitted_to_journal=context['journal'].name).order_by('-latest_activity') + return context def info_for_authors(request, doi_label): journal = get_object_or_404(Journal, doi_label=doi_label) - context = { - 'journal': journal - } + context = {'journal': journal} return render(request, 'journals/%s_info_for_authors.html' % doi_label, context) def about(request, doi_label): journal = get_object_or_404(Journal, doi_label=doi_label) - context = { - 'journal': journal - } + context = {'journal': journal} return render(request, 'journals/%s_about.html' % doi_label, context) def issue_detail(request, doi_label): - issue = Issue.objects.get_published(doi_label=doi_label) + issue = get_object_or_404(Issue.objects.published(), doi_label=doi_label) journal = issue.in_volume.in_journal - papers = issue.publication_set.order_by('paper_nr') - next_issue = (Issue.objects.published(in_volume__in_journal=journal, - start_date__gt=issue.start_date) - .order_by('start_date').first()) - prev_issue = (Issue.objects.published(in_volume__in_journal=journal, - start_date__lt=issue.start_date) - .order_by('start_date').last()) + papers = issue.publications.published().order_by('paper_nr') + next_issue = Issue.objects.published().filter( + in_volume__in_journal=journal, start_date__gt=issue.start_date + ).order_by('start_date').first() + prev_issue = Issue.objects.published().filter( + in_volume__in_journal=journal, start_date__lt=issue.start_date + ).order_by('start_date').last() + context = { 'issue': issue, 'prev_issue': prev_issue, @@ -160,171 +187,135 @@ def issue_detail(request, doi_label): ####################### # Publication process # ####################### - -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -@transaction.atomic -def initiate_publication(request): +class PublicationGrantsView(PermissionsMixin, UpdateView): """ - Called by an Editorial Administrator. - Publish the manuscript after proofs have been accepted. - This method prefills a ValidatePublicationForm for further - processing (verification in validate_publication method). + Add/update grants associated to a Publication. """ - initiate_publication_form = InitiatePublicationForm(request.POST or None) - if initiate_publication_form.is_valid(): - submission = initiate_publication_form.cleaned_data['accepted_submission'] - current_issue = initiate_publication_form.cleaned_data['to_be_issued_in'] - - # Determine next available paper number: - paper_nr = Publication.objects.filter(in_issue__in_volume=current_issue.in_volume).count() - paper_nr += 1 - if paper_nr > 999: - raise PaperNumberingError(paper_nr) - - # Build form data - doi_label = ( - current_issue.in_volume.in_journal.name - + '.' + str(current_issue.in_volume.number) - + '.' + str(current_issue.number) + '.' + paper_nr_string(paper_nr) - ) - doi_string = '10.21468/' + doi_label - BiBTeX_entry = ( - '@Article{' + doi_label + ',\n' - '\ttitle={{' + submission.title + '}},\n' - '\tauthor={' + submission.author_list.replace(',', ' and') + '},\n' - '\tjournal={' - + current_issue.in_volume.in_journal.get_abbreviation_citation() - + '},\n' - '\tvolume={' + str(current_issue.in_volume.number) + '},\n' - '\tissue={' + str(current_issue.number) + '},\n' - '\tpages={' + paper_nr_string(paper_nr) + '},\n' - '\tyear={' + current_issue.until_date.strftime('%Y') + '},\n' - '\tpublisher={SciPost},\n' - '\tdoi={' + doi_string + '},\n' - '\turl={https://scipost.org/' + doi_string + '},\n' - '}\n' - ) - initial = { - 'accepted_submission': submission, - 'in_issue': current_issue, - 'paper_nr': paper_nr, - 'discipline': submission.discipline, - 'domain': submission.domain, - 'subject_area': submission.subject_area, - 'secondary_areas': submission.secondary_areas, - 'title': submission.title, - 'author_list': submission.author_list, - 'abstract': submission.abstract, - 'BiBTeX_entry': BiBTeX_entry, - 'doi_label': doi_label, - 'acceptance_date': submission.acceptance_date, - 'submission_date': submission.submission_date, - 'publication_date': timezone.now(), - } - validate_publication_form = ValidatePublicationForm(initial=initial) - context = {'validate_publication_form': validate_publication_form} - return render(request, 'journals/validate_publication.html', context) - - context = {'initiate_publication_form': initiate_publication_form} - return render(request, 'journals/initiate_publication.html', context) + permission_required = 'scipost.can_draft_publication' + queryset = Publication.objects.drafts() + slug_field = slug_url_kwarg = 'doi_label' + form_class = PublicationGrantsForm + template_name = 'journals/grants_form.html' -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -@transaction.atomic -def validate_publication(request): +class PublicationGrantsRemovalView(PermissionsMixin, DetailView): """ - This creates a Publication instance from the ValidatePublicationForm, - pre-filled by the initiate_publication method above. + Remove grant associated to a Publication. """ - # TODO: move from uploads to Journal folder - # TODO: create metadata - # TODO: set DOI, register with Crossref - # TODO: add funding info - context = {} - validate_publication_form = ValidatePublicationForm(request.POST or None, - request.FILES or None) - if validate_publication_form.is_valid(): - publication = validate_publication_form.save() - - # Fill remaining data - submission = publication.accepted_submission - - for submission_author in submission.authors.all(): - PublicationAuthorsTable.objects.create( - publication=publication, contributor=submission_author) - publication.authors_claims.add(*submission.authors_claims.all()) - publication.authors_false_claims.add(*submission.authors_false_claims.all()) - - # Add Institutions to the publication - for author in publication.authors_registered.all(): - for current_affiliation in author.affiliations.active(): - publication.institutions.add(current_affiliation.institution) - - # Save the beast - publication.save() + permission_required = 'scipost.can_draft_publication' + queryset = Publication.objects.drafts() + slug_field = slug_url_kwarg = 'doi_label' - # Move file to final location - initial_path = publication.pdf_file.path - new_dir = (settings.MEDIA_ROOT + publication.in_issue.path + '/' - + publication.get_paper_nr()) - new_path = new_dir + '/' + publication.doi_label.replace('.', '_') + '.pdf' - os.makedirs(new_dir) - os.rename(initial_path, new_path) - publication.pdf_file.name = new_path - publication.save() + def get(self, request, *args, **kwargs): + super().get(request, *args, **kwargs) + grant = get_object_or_404(Grant, id=kwargs.get('grant_id')) + self.object.grants.remove(grant) + return redirect(reverse('journals:update_grants', args=(self.object.doi_label,))) - # Mark the submission as having been published: - submission.published_as = publication - submission.status = 'published' - submission.save() - - # Update ProductionStream - if hasattr(submission, 'production_stream'): - stream = submission.production_stream - stream.status = PROOFS_PUBLISHED - stream.save() - if request.user.production_user: - prodevent = ProductionEvent( - stream=stream, - event='status', - comments=' published the manuscript.', - noted_by=request.user.production_user - ) - prodevent.save() - notify_stream_status_change(request.user, stream, False) - - # TODO: Create a Commentary Page - # Email authors - JournalUtils.load({'publication': publication}) - JournalUtils.send_authors_paper_published_email() - - # Add SubmissionEvents - submission.add_general_event('The Submission has been published as %s.' - % publication.doi_label) - - messages.success(request, 'The publication has been validated.') + +@permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +def publication_authors_ordering(request, doi_label): + publication = get_object_or_404(Publication, doi_label=doi_label) + formset = PublicationAuthorOrderingFormSet( + request.POST or None, queryset=publication.authors.order_by('order')) + if formset.is_valid(): + formset.save() + messages.success(request, 'Author ordering updated') return redirect(publication.get_absolute_url()) - else: - context['errormessage'] = 'The form was invalid.' + context = { + 'formset': formset, + 'publication': publication, + } + return render(request, 'journals/publication_authors_form.html', context) + - context['validate_publication_form'] = validate_publication_form - return render(request, 'journals/validate_publication.html', context) +class DraftPublicationUpdateView(PermissionsMixin, UpdateView): + """ + Any Production Officer or Administrator can draft a new publication without publishing here. + The actual publishing is done lin a later stadium, after the draft has been finished. + """ + permission_required = 'scipost.can_draft_publication' + queryset = Publication.objects.unpublished() + slug_url_kwarg = 'arxiv_identifier_w_vn_nr' + slug_field = 'accepted_submission__arxiv_identifier_w_vn_nr' + form_class = DraftPublicationForm + template_name = 'journals/publication_form.html' + + def get_object(self, queryset=None): + try: + publication = Publication.objects.get( + accepted_submission__arxiv_identifier_w_vn_nr=self.kwargs.get( + 'arxiv_identifier_w_vn_nr')) + except Publication.DoesNotExist: + if Submission.objects.accepted().filter(arxiv_identifier_w_vn_nr=self.kwargs.get( + 'arxiv_identifier_w_vn_nr')).exists(): + return None + raise Http404('No accepted Submission found') + if publication.status == STATUS_DRAFT: + return publication + if self.request.user.has_perm('scipost.can_publish_accepted_submission'): + return publication + raise Http404('Found Publication is not in draft') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['arxiv_identifier_w_vn_nr'] = self.kwargs.get('arxiv_identifier_w_vn_nr') + kwargs['issue_id'] = self.request.GET.get('issue') + return kwargs + + +class DraftPublicationApprovalView(PermissionsMixin, UpdateView): + permission_required = 'scipost.can_draft_publication' + queryset = Publication.objects.drafts() + slug_field = slug_url_kwarg = 'doi_label' + form_class = DraftPublicationApprovalForm + template_name = 'journals/publication_approval_form.html' + + +@method_decorator(transaction.atomic, name='dispatch') +class PublicationPublishView(PermissionsMixin, RequestViewMixin, UpdateView): + permission_required = 'scipost.can_publish_accepted_submission' + queryset = Publication.objects.unpublished() + slug_field = slug_url_kwarg = 'doi_label' + form_class = PublicationPublishForm + template_name = 'journals/publication_publish_form.html' @permission_required('scipost.can_publish_accepted_submission', return_403=True) -def manage_metadata(request, issue_doi_label=None, doi_label=None): - issues = Issue.objects.all().order_by('-until_date') - publications = Publication.objects.all() +def manage_metadata(request, doi_label=None, issue_doi_label=None, journal_doi_label=None): + journal = None + journals = Journal.objects.all() + issue = None + if doi_label: - issue_doi_label = issue_doi_label_from_doi_label(doi_label) - if issue_doi_label: - publications = publications.filter(in_issue__doi_label=issue_doi_label) - publications = publications.order_by('-publication_date', '-paper_nr') + publications = get_list_or_404(Publication, doi_label=doi_label) + journal = publications[0].get_journal() + elif issue_doi_label: + issue = get_object_or_404(Issue, doi_label=issue_doi_label) + if issue.in_volume: + journal = issue.in_volume.in_journal + else: + journal = issue.in_journal + publications = issue.publications.all() + elif journal_doi_label: + journal = get_object_or_404(Journal, doi_label=journal_doi_label) + publications = Publication.objects.for_journal(journal.name) + else: + # Limit the amount of Publications to still an idiot size + publications = Publication.objects.all()[:50] + + # Speeds up operations by reducing the number of queries + if not isinstance(publications, list): + publications = publications.prefetch_related( + 'authors', 'funders_generic', 'deposit_set', 'doajdeposit_set') + associate_grant_form = GrantSelectForm() associate_generic_funder_form = FunderSelectForm() context = { - 'issues': issues, + 'journal': journal, + 'journals': journals, 'issue_doi_label': issue_doi_label, + 'journal_doi_label': journal_doi_label, 'publications': publications, 'associate_grant_form': associate_grant_form, 'associate_generic_funder_form': associate_generic_funder_form, @@ -332,26 +323,7 @@ def manage_metadata(request, issue_doi_label=None, doi_label=None): return render(request, 'journals/manage_metadata.html', context) -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -def mark_first_author(request, publication_id, author_object_id): - publication = get_object_or_404(Publication, id=publication_id) - author_object = get_object_or_404(publication.authors, id=author_object_id) - - # Redo ordering - author_object.order = 1 - author_object.save() - author_objects = publication.authors.exclude(id=author_object.id) - count = 2 - for author in author_objects: - author.order = count - author.save() - count += 1 - messages.success(request, 'Marked {} first author'.format(author_object)) - return redirect(reverse('journals:manage_metadata', - kwargs={'doi_label': publication.doi_label})) - - -@permission_required('scipost.can_publish_accepted_submission', return_403=True) +@permission_required('scipost.can_draft_publication', return_403=True) @transaction.atomic def add_author(request, doi_label, contributor_id=None, unregistered_author_id=None): """ @@ -361,6 +333,9 @@ def add_author(request, doi_label, contributor_id=None, unregistered_author_id=N This is important for the Crossref metadata, in which all authors must appear. """ publication = get_object_or_404(Publication, doi_label=doi_label) + if not publication.is_draft and not request.user.has_perm('can_publish_accepted_submission'): + raise Http404('You do not have permission to edit this non-draft Publication') + if contributor_id: contributor = get_object_or_404(Contributor, id=contributor_id) PublicationAuthorsTable.objects.create(contributor=contributor, publication=publication) @@ -393,38 +368,16 @@ def add_author(request, doi_label, contributor_id=None, unregistered_author_id=N return render(request, 'journals/add_author.html', context) -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -@transaction.atomic -def create_citation_list_metadata(request, doi_label): - """ - Called by an Editorial Administrator. - This populates the citation_list dictionary entry - in the metadata field in a Publication instance. - """ - publication = get_object_or_404(Publication, doi_label=doi_label) - bibitems_form = CitationListBibitemsForm(request.POST or None, request.FILES or None) - if bibitems_form.is_valid(): - publication.metadata['citation_list'] = bibitems_form.extract_dois() - publication.save() - messages.success(request, 'Updated citation list') - return redirect(reverse('journals:create_citation_list_metadata', - kwargs={'doi_label': publication.doi_label})) - context = { - 'publication': publication, - 'bibitems_form': bibitems_form, - 'citation_list': publication.metadata.get('citation_list', '') - } - return render(request, 'journals/create_citation_list_metadata.html', context) - - -@permission_required('scipost.can_publish_accepted_submission', return_403=True) +@permission_required('scipost.can_draft_publication', return_403=True) def update_references(request, doi_label): """ Update the References for a certain Publication. """ publication = get_object_or_404(Publication, doi_label=doi_label) - references = publication.references.all() + if not publication.is_draft and not request.user.has_perm('can_publish_accepted_submission'): + raise Http404('You do not have permission to edit this non-draft Publication') + references = publication.references.all() formset = ReferenceFormSet(request.POST or None, queryset=references, publication=publication, extra=request.GET.get('extra')) @@ -443,34 +396,20 @@ def update_references(request, doi_label): return render(request, 'journals/update_references.html', context) -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -@transaction.atomic -def create_funding_info_metadata(request, doi_label): +class CitationUpdateView(PublicationMixin, ProdSupervisorPublicationPermissionMixin, UpdateView): """ - Called by an Editorial Administrator. - This populates the funding_info dictionary entry - in the metadata field in a Publication instance. + Populates the citation_list dictionary entry in the metadata field in a Publication instance. """ - publication = get_object_or_404(Publication, doi_label=doi_label) - - funding_statement = publication.metadata.get('funding_statement', '') - initial = { - 'funding_statement': funding_statement, - } - form = FundingInfoForm(request.POST or None, instance=publication, initial=initial) - if form.is_valid(): - form.save() - messages.success(request, 'Updated funding info') - return redirect(reverse('journals:create_funding_info_metadata', - kwargs={'doi_label': publication.doi_label})) + form_class = CitationListBibitemsForm + template_name = 'journals/create_citation_list_metadata.html' - context = { - 'publication': publication, - 'funding_info_form': form, - 'funding_statement': funding_statement, - } - return render(request, 'journals/create_funding_info_metadata.html', context) +class FundingInfoView(PublicationMixin, ProdSupervisorPublicationPermissionMixin, UpdateView): + """ + Add/update funding statement to the xml_metadata + """ + form_class = FundingInfoForm + template_name = 'journals/create_funding_info_metadata.html' @permission_required('scipost.can_publish_accepted_submission', return_403=True) @@ -507,195 +446,19 @@ def add_generic_funder(request, doi_label): kwargs={'doi_label': doi_label})) -@permission_required('scipost.can_publish_accepted_submission', return_403=True) -@transaction.atomic -def create_metadata_xml(request, doi_label): +class CreateMetadataXMLView(PublicationMixin, + ProdSupervisorPublicationPermissionMixin, + UpdateView): """ - To be called by an EdAdmin after the citation_list, - funding_info entries have been filled. - Populates the metadata_xml field of a Publication instance. + To be called by an EdAdmin (or Production Supervisor) after the citation_list, funding_info + entries have been filled. Populates the metadata_xml field of a Publication instance. The contents can then be sent to Crossref for registration. """ - publication = get_object_or_404(Publication, doi_label=doi_label) - - # create a doi_batch_id - salt = "" - for i in range(5): - salt = salt + random.choice(string.ascii_letters) - salt = salt.encode('utf8') - idsalt = publication.title[:10] - idsalt = idsalt.encode('utf8') - doi_batch_id = hashlib.sha1(salt+idsalt).hexdigest() - - initial = {'metadata_xml': ''} - initial['metadata_xml'] += ( - '<?xml version="1.0" encoding="UTF-8"?>\n' - '<doi_batch version="4.4.0" xmlns="http://www.crossref.org/schema/4.4.0" ' - 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' - 'xmlns:fr="http://www.crossref.org/fundref.xsd" ' - 'xsi:schemaLocation="http://www.crossref.org/schema/4.4.0 ' - 'http://www.crossref.org/shema/deposit/crossref4.4.0.xsd" ' - 'xmlns:ai="http://www.crossref.org/AccessIndicators.xsd">\n' - '<head>\n' - '<doi_batch_id>' + str(doi_batch_id) + '</doi_batch_id>\n' - '<timestamp>' + timezone.now().strftime('%Y%m%d%H%M%S') + '</timestamp>\n' - '<depositor>\n' - '<depositor_name>scipost</depositor_name>\n' - '<email_address>' + settings.CROSSREF_DEPOSIT_EMAIL + '</email_address>\n' - '</depositor>\n' - '<registrant>scipost</registrant>\n' - '</head>\n' - '<body>\n' - '<journal>\n' - '<journal_metadata>\n' - '<full_title>' + publication.in_issue.in_volume.in_journal.get_name_display() - + '</full_title>\n' - '<abbrev_title>' - + publication.in_issue.in_volume.in_journal.get_abbreviation_citation() + - '</abbrev_title>\n' - '<issn media_type=\'electronic\'>' + publication.in_issue.in_volume.in_journal.issn - + '</issn>\n' - '<doi_data>\n' - '<doi>' + publication.in_issue.in_volume.in_journal.doi_string + '</doi>\n' - '<resource>https://scipost.org/' - + publication.in_issue.in_volume.in_journal.doi_string + '</resource>\n' - '</doi_data>\n' - '</journal_metadata>\n' - '<journal_issue>\n' - '<publication_date media_type=\'online\'>\n' - '<year>' + publication.publication_date.strftime('%Y') + '</year>\n' - '</publication_date>\n' - '<journal_volume>\n' - '<volume>' + str(publication.in_issue.in_volume.number) + '</volume>\n' - '</journal_volume>\n' - '<issue>' + str(publication.in_issue.number) + '</issue>\n' - '</journal_issue>\n' - '<journal_article publication_type=\'full_text\'>\n' - '<titles><title>' + publication.title + '</title></titles>\n' - ) - - # Precondition: all authors MUST be listed in authors field of publication instance, - # this to be checked by EdAdmin before publishing. - initial['metadata_xml'] += '<contributors>\n' - for author_object in publication.authors.all(): - if author_object.order == 1: - initial['metadata_xml'] += ( - '<person_name sequence=\'first\' contributor_role=\'author\'> ' - '<given_name>' + author_object.first_name + '</given_name> ' - '<surname>' + author_object.last_name + '</surname> ' - ) - else: - initial['metadata_xml'] += ( - '<person_name sequence=\'additional\' contributor_role=\'author\'> ' - '<given_name>' + author_object.first_name + '</given_name> ' - '<surname>' + author_object.last_name + '</surname> ' - ) - if author_object.contributor and author_object.contributor.orcid_id: - initial['metadata_xml'] += ( - '<ORCID>http://orcid.org/' + author_object.contributor.orcid_id + '</ORCID>' - ) - initial['metadata_xml'] += '</person_name>\n' - initial['metadata_xml'] += '</contributors>\n' - - initial['metadata_xml'] += ( - '<publication_date media_type=\'online\'>\n' - '<month>' + publication.publication_date.strftime('%m') + '</month>' - '<day>' + publication.publication_date.strftime('%d') + '</day>' - '<year>' + publication.publication_date.strftime('%Y') + '</year>' - '</publication_date>\n' - '<publisher_item><item_number item_number_type="article_number">' - + paper_nr_string(publication.paper_nr) + - '</item_number></publisher_item>\n' - '<crossmark>\n' - '<crossmark_policy>10.21468/SciPost.CrossmarkPolicy</crossmark_policy>\n' - '<crossmark_domains>\n' - '<crossmark_domain><domain>scipost.org</domain></crossmark_domain>\n' - '</crossmark_domains>\n' - '<crossmark_domain_exclusive>false</crossmark_domain_exclusive>\n' - ) - funders = (Funder.objects.filter(grant__in=publication.grants.all()) - | publication.funders_generic.all()).distinct() - nr_funders = funders.count() - initial['metadata_xml'] += '<custom_metadata>\n' - if nr_funders > 0: - initial['metadata_xml'] += '<fr:program name="fundref">\n' - for funder in funders: - if nr_funders > 1: - initial['metadata_xml'] += '<fr:assertion name="fundgroup">\n' - initial['metadata_xml'] += ( - '<fr:assertion name="funder_name">' + funder.name + '\n' - '<fr:assertion name="funder_identifier">' - + funder.identifier + '</fr:assertion>\n' - '</fr:assertion>\n') - for grant in publication.grants.all(): - if grant.funder == funder: - initial['metadata_xml'] += ( - '<fr:assertion name="award_number">' - + grant.number + '</fr:assertion>\n') - if nr_funders > 1: - initial['metadata_xml'] += '</fr:assertion>\n' - initial['metadata_xml'] += '</fr:program>\n' - initial['metadata_xml'] += ( - '<ai:program name="AccessIndicators">\n' - '<ai:license_ref>' + publication.get_cc_license_URI() + - '</ai:license_ref>\n' - '</ai:program>\n' - ) - initial['metadata_xml'] += '</custom_metadata>\n' - initial['metadata_xml'] += ( - '</crossmark>\n' - '<archive_locations><archive name="CLOCKSS"></archive></archive_locations>\n' - '<doi_data>\n' - '<doi>' + publication.doi_string + '</doi>\n' - '<resource>https://scipost.org/' + publication.doi_string + '</resource>\n' - '<collection property="crawler-based">\n' - '<item crawler="iParadigms">\n' - '<resource>https://scipost.org/' - + publication.doi_string + '/pdf</resource>\n' - '</item></collection>\n' - '<collection property="text-mining">\n' - '<item><resource mime_type="application/pdf">' - 'https://scipost.org/' + publication.doi_string + '/pdf</resource></item>\n' - '</collection>' - '</doi_data>\n' - ) - try: - if publication.metadata['citation_list']: - initial['metadata_xml'] += '<citation_list>\n' - for ref in publication.metadata['citation_list']: - initial['metadata_xml'] += ( - '<citation key="' + ref['key'] + '">' - '<doi>' + ref['doi'] + '</doi>' - '</citation>\n' - ) - initial['metadata_xml'] += '</citation_list>\n' - except KeyError: - pass - initial['metadata_xml'] += ( - '</journal_article>\n' - '</journal>\n' - ) - initial['metadata_xml'] += '</body>\n</doi_batch>' - - create_metadata_xml_form = CreateMetadataXMLForm(request.POST or None, - instance=publication, - initial=initial) - if create_metadata_xml_form.is_valid(): - create_metadata_xml_form.save() - messages.success(request, 'Metadata XML saved') - return redirect(reverse('journals:manage_metadata', - kwargs={'doi_label': doi_label})) - - publication.latest_metadata_update = timezone.now() - publication.save() - context = { - 'publication': publication, - 'create_metadata_xml_form': create_metadata_xml_form, - } - return render(request, 'journals/create_metadata_xml.html', context) + form_class = CreateMetadataXMLForm + template_name = 'journals/create_metadata_xml.html' -@permission_required('scipost.can_publish_accepted_submission', return_403=True) +@permission_required('scipost.can_draft_publication', return_403=True) @transaction.atomic def metadata_xml_deposit(request, doi_label, option='test'): """ @@ -704,6 +467,10 @@ def metadata_xml_deposit(request, doi_label, option='test'): Makes use of the python requests module. """ publication = get_object_or_404(Publication, doi_label=doi_label) + if not publication.is_draft and not request.user.has_perm('can_publish_accepted_submission'): + raise Http404('You do not have permission to access this non-draft Publication') + if not request.user.has_perm('can_publish_accepted_submission') and option != 'test': + raise PermissionDenied('You do not have permission to do real Crossref deposits') if publication.metadata_xml is None: messages.warning( @@ -712,13 +479,27 @@ def metadata_xml_deposit(request, doi_label, option='test'): return redirect(reverse('journals:create_metadata_xml', kwargs={'doi_label': publication.doi_label})) - timestamp = (publication.metadata_xml.partition( - '<timestamp>'))[2].partition('</timestamp>')[0] - doi_batch_id = (publication.metadata_xml.partition( - '<doi_batch_id>'))[2].partition('</doi_batch_id>')[0] - path = (settings.MEDIA_ROOT + publication.in_issue.path + '/' - + publication.get_paper_nr() + '/' + publication.doi_label.replace('.', '_') - + '_Crossref_' + timestamp + '.xml') + timestamp = publication.metadata_xml.partition( + '<timestamp>')[2].partition('</timestamp>')[0] + doi_batch_id = publication.metadata_xml.partition( + '<doi_batch_id>')[2].partition('</doi_batch_id>')[0] + + # Find Crossref xml files + path = settings.MEDIA_ROOT + if publication.in_issue: + path += '{issue_path}/{paper_nr}/{doi_label}_Crossref'.format( + issue_path=publication.in_issue.path, + paper_nr=publication.get_paper_nr(), + doi_label=publication.doi_label.replace('.', '_')) + + if publication.in_journal: + path += 'SCIPOST_JOURNALS/{journal_name}/{paper_nr}/{doi_label}_Crossref'.format( + journal_name=publication.in_journal.name, + paper_nr=publication.get_paper_nr(), + doi_label=publication.doi_label.replace('.', '_')) + + path_wo_timestamp = path + '.xml' + path += '_{timestamp}.xml'.format(timestamp=timestamp) valid = True response_headers = None @@ -729,7 +510,7 @@ def metadata_xml_deposit(request, doi_label, option='test'): else: # New deposit, go for it. if option == 'deposit' and not settings.DEBUG: - # CAUTION: Real deposit only on production (non-debug-mode) + # CAUTION: Real deposit only on production! url = 'http://doi.crossref.org/servlet/deposit' else: url = 'http://test.crossref.org/servlet/deposit' @@ -741,7 +522,9 @@ def metadata_xml_deposit(request, doi_label, option='test'): 'login_passwd': settings.CROSSREF_LOGIN_PASSWORD, } files = { - 'fname': ('metadata.xml', publication.metadata_xml.encode('utf-8'), 'multipart/form-data') + 'fname': ('metadata.xml', + publication.metadata_xml.encode('utf-8'), + 'multipart/form-data') } r = requests.post(url, params=params, files=files) response_headers = r.headers @@ -757,24 +540,14 @@ def metadata_xml_deposit(request, doi_label, option='test'): deposit.response_text = r.text # Save the filename with timestamp - path_with_timestamp = '{issue}/{paper}/{doi}_Crossref_{timestamp}.xml'.format( - issue=publication.in_issue.path, - paper=publication.get_paper_nr(), - doi=publication.doi_label.replace('.', '_'), - timestamp=timestamp) - f = open(settings.MEDIA_ROOT + path_with_timestamp, 'w', encoding='utf-8') + f = open(settings.MEDIA_ROOT + path, 'w', encoding='utf-8') f.write(publication.metadata_xml) f.close() - # Copy file - path_without_timestamp = '{issue}/{paper}/{doi}_Crossref.xml'.format( - issue=publication.in_issue.path, - paper=publication.get_paper_nr(), - doi=publication.doi_label.replace('.', '_')) - shutil.copyfile(settings.MEDIA_ROOT + path_with_timestamp, - settings.MEDIA_ROOT + path_without_timestamp) - - deposit.metadata_xml_file = path_with_timestamp + # Update Crossref timestamp-free file to latest deposit + shutil.copyfile(settings.MEDIA_ROOT + path, + settings.MEDIA_ROOT + path_wo_timestamp) + deposit.metadata_xml_file = path deposit.save() publication.latest_crossref_deposit = timezone.now() publication.save() @@ -831,11 +604,24 @@ def metadata_DOAJ_deposit(request, doi_label): 'DOAJ metadata before depositing.' % publication.doi_label) return redirect(reverse('journals:manage_metadata')) - timestamp = (publication.metadata_xml.partition( - '<timestamp>'))[2].partition('</timestamp>')[0] - path = (settings.MEDIA_ROOT + publication.in_issue.path + '/' - + publication.get_paper_nr() + '/' + publication.doi_label.replace('.', '_') - + '_DOAJ_' + timestamp + '.json') + timestamp = publication.metadata_xml.partition('<timestamp>')[2].partition('</timestamp>')[0] + + # Find DOAJ xml files + path = settings.MEDIA_ROOT + if publication.in_issue: + path += '{issue_path}/{paper_nr}/{doi_label}_DOAJ'.format( + issue_path=publication.in_issue.path, + paper_nr=publication.get_paper_nr(), + doi_label=publication.doi_label.replace('.', '_')) + elif publication.in_journal: + path += 'SCIPOST_JOURNALS/{journal_name}/{paper_nr}/{doi_label}_DOAJ'.format( + journal_name=publication.in_journal.name, + paper_nr=publication.get_paper_nr(), + doi_label=publication.doi_label.replace('.', '_')) + + path_wo_timestamp = path + '.json' + path += '_{timestamp}.json'.format(timestamp=timestamp) + if os.path.isfile(path): errormessage = 'The metadata file for this metadata timestamp already exists' return render(request, 'scipost/error.html', context={'errormessage': errormessage}) @@ -857,25 +643,16 @@ def metadata_DOAJ_deposit(request, doi_label): deposit.response_text = r.text # Save a copy to the filename with and without timestamp - path_with_timestamp = '{issue}/{paper}/{doi}_DOAJ_{timestamp}.json'.format( - issue=publication.in_issue.path, - paper=publication.get_paper_nr(), - doi=publication.doi_label.replace('.', '_'), - timestamp=timestamp) - f = open(settings.MEDIA_ROOT + path_with_timestamp, 'w') + f = open(settings.MEDIA_ROOT + path, 'w') f.write(json.dumps(publication.metadata_DOAJ)) f.close() # Copy file - path_without_timestamp = '{issue}/{paper}/{doi}_DOAJ.json'.format( - issue=publication.in_issue.path, - paper=publication.get_paper_nr(), - doi=publication.doi_label.replace('.', '_')) - shutil.copyfile(settings.MEDIA_ROOT + path_with_timestamp, - settings.MEDIA_ROOT + path_without_timestamp) + shutil.copyfile(settings.MEDIA_ROOT + path, + settings.MEDIA_ROOT + path_wo_timestamp) # Save the database entry - deposit.metadata_DOAJ_file = path_with_timestamp + deposit.metadata_DOAJ_file = path deposit.save() messages.success(request, '<h3>%s</h3>Successfull deposit of metadata DOAJ.' @@ -1253,7 +1030,7 @@ def email_object_made_citable(request, **kwargs): try: publication = Publication.objects.get( accepted_submission__arxiv_identifier_wo_vn_nr=_object.submission.arxiv_identifier_wo_vn_nr) - publication_citation = publication.citation() + publication_citation = publication.citation publication_doi = publication.doi_string except Publication.DoesNotExist: pass @@ -1299,8 +1076,20 @@ def author_reply_detail(request, doi_label): def publication_detail(request, doi_label): - publication = Publication.objects.get_published(doi_label=doi_label) - journal = publication.in_issue.in_volume.in_journal + """ + The actual Publication detail page. This is visible for everyone if published or + visible for Production Supervisors and Administrators if in draft. + """ + publication = get_object_or_404(Publication, doi_label=doi_label) + if not publication.is_published and not request.user.has_perm('scipost.can_draft_publication'): + raise Http404('Publication is not publicly visible') + + if publication.in_issue: + journal = publication.in_issue.in_volume.in_journal + elif publication.in_journal: + journal = publication.in_journal + else: + raise Http404('Publication configuration is valid') context = { 'publication': publication, @@ -1310,7 +1099,14 @@ def publication_detail(request, doi_label): def publication_detail_pdf(request, doi_label): - publication = Publication.objects.get_published(doi_label=doi_label) + """ + The actual Publication pdf. This is visible for everyone if published or + visible for Production Supervisors and Administrators if in draft. + """ + publication = get_object_or_404(Publication, doi_label=doi_label) + if not publication.is_published and not request.user.has_perm('scipost.can_draft_publication'): + raise Http404('Publication is not publicly visible') + response = HttpResponse(publication.pdf_file.read(), content_type='application/pdf') response['Content-Disposition'] = ('filename=' + publication.doi_label.replace('.', '_') + '.pdf') @@ -1321,11 +1117,11 @@ def publication_detail_pdf(request, doi_label): # Feed DOIs to arXiv # ###################### -""" -This method provides arXiv with the doi and journal ref of the 100 most recent -publications in the journal specified by doi_label. -""" def arxiv_doi_feed(request, doi_label): + """ + This method provides arXiv with the doi and journal ref of the 100 most recent + publications in the journal specified by doi_label. + """ journal = get_object_or_404(Journal, doi_label=doi_label) feedxml = ('<preprint xmlns="http://arxiv.org/doi_feed" ' 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' diff --git a/mailing_lists/templates/mailing_lists/mailchimplist_form.html b/mailing_lists/templates/mailing_lists/mailchimplist_form.html index e0715f011020d5d2aa060853e6173ff62cc0f9ad..ffdaf457c2df57cd819e210b8861af345f84eb0c 100644 --- a/mailing_lists/templates/mailing_lists/mailchimplist_form.html +++ b/mailing_lists/templates/mailing_lists/mailchimplist_form.html @@ -30,7 +30,7 @@ <form method="post"> {% csrf_token %} {{form|bootstrap}} - <input type="submit" value="Update" class="btn btn-secondary" /> + <input type="submit" value="Update" class="btn btn-outline-secondary" /> </form> {% if request.GET.bulkid %} <div class="mb-3 mt-5"> diff --git a/mails/mixins.py b/mails/mixins.py index e34dd97123125c66c502413ace9248fba4a3d93e..8b88a62e291e80b7d640ff13ffd5ec8d3175721e 100644 --- a/mails/mixins.py +++ b/mails/mixins.py @@ -4,7 +4,6 @@ import inspect from html2text import HTML2Text from django.core.mail import EmailMultiAlternatives -from django.contrib import messages from django.contrib.auth import get_user_model from django.conf import settings from django.template import loader @@ -12,79 +11,6 @@ from django.template import loader from scipost.models import Contributor -from . import forms - - -class MailEditorMixin: - """ - Use MailEditorMixin in edit CBVs to automatically implement the mail editor as - a post-form_valid hook. - - The view must specify the `mail_code` variable. - """ - object = None - mail_form = None - has_permission_to_send_mail = True - alternative_from_address = None # Tuple: ('from_name', 'from_address') - - def __init__(self, *args, **kwargs): - if not self.mail_code: - raise AttributeError(self.__class__.__name__ + ' object has no attribute `mail_code`') - super().__init__(*args, **kwargs) - - def get_template_names(self): - """ - The mail editor form has its own template. - """ - if self.mail_form and not self.mail_form.is_valid(): - return ['mails/mail_form.html'] - return super().get_template_names() - - def post(self, request, *args, **kwargs): - """ - Handle POST requests, but interpect the data if the mail form data isn't valid. - """ - if not self.has_permission_to_send_mail: - # Don't use the mail form; don't send out the mail. - return super().post(request, *args, **kwargs) - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - self.mail_form = forms.EmailTemplateForm(request.POST or None, - mail_code=self.mail_code, - instance=self.object) - if self.mail_form.is_valid(): - return self.form_valid(form) - - return self.render_to_response( - self.get_context_data(form=self.mail_form, - transfer_data_form=forms.HiddenDataForm(form))) - else: - return self.form_invalid(form) - - def form_valid(self, form): - """ - If both the regular form and mailing form are valid, save the form and run the mail form. - """ - # Don't use the mail form; don't send out the mail. - if not self.has_permission_to_send_mail: - return super().form_valid(form) - - if self.alternative_from_address: - # Set different from address if given. - self.mail_form.set_alternative_sender( - self.alternative_from_address[0], self.alternative_from_address[1]) - - response = super().form_valid(form) - try: - self.mail_form.send() - except AttributeError: - # self.mail_form is None - raise AttributeError('Did you check the order in which MailEditorMixin is used?') - messages.success(self.request, 'Mail sent') - return response - - class MailUtilsMixin: """ This mixin takes care of inserting the default data into the Utils or Form. @@ -126,14 +52,7 @@ class MailUtilsMixin: self.mail_template = mail_template.render(kwargs) # Gather Recipients data - self.original_recipient = '' - if self.object: - recipient = self.object - for attr in self.mail_data.get('to_address').split('.'): - recipient = getattr(recipient, attr) - if inspect.ismethod(recipient): - recipient = recipient() - self.original_recipient = recipient + self.original_recipient = self._validate_single_entry(self.mail_data.get('to_address'))[0] self.subject = self.mail_data['subject'] @@ -148,18 +67,20 @@ class MailUtilsMixin: # Email string return [entry] else: - bcc_to = self.object + mail_to = self.object for attr in entry.split('.'): try: - bcc_to = getattr(bcc_to, attr) + mail_to = getattr(mail_to, attr) + if inspect.ismethod(mail_to): + mail_to = mail_to() except AttributeError: - # Invalid property, don't use bcc + # Invalid property/mail return [] - if not isinstance(bcc_to, list): - return [bcc_to] + if not isinstance(mail_to, list): + return [mail_to] else: - return bcc_to + return mail_to elif re.match("[^@]+@[^@]+\.[^@]+", entry): return [entry] @@ -170,8 +91,9 @@ class MailUtilsMixin: """ # Get recipients list. Try to send through BCC to prevent privacy issues! self.bcc_list = [] - for bcc_entry in self.mail_data.get('bcc_to', '').split(','): - self.bcc_list += self._validate_single_entry(bcc_entry) + if self.mail_data.get('bcc_to'): + for bcc_entry in self.mail_data['bcc_to'].split(','): + self.bcc_list += self._validate_single_entry(bcc_entry) def validate_recipients(self): # Check the send list diff --git a/mails/templates/mail_templates/publication_ready.html b/mails/templates/mail_templates/publication_ready.html new file mode 100644 index 0000000000000000000000000000000000000000..a3ebebbd5a05a8bd1103ead6d5db36f23ee1d4ca --- /dev/null +++ b/mails/templates/mail_templates/publication_ready.html @@ -0,0 +1,14 @@ +<p> + The following Publication is drafted and ready for publication. +</p> +<p> + <a href="https://scipost.org/{{ publication.get_absolute_url }}">{{ publication.title }}</a><br> + by {{ publication.author_list }} +</p> +<p> + Please review the Publication and proceed with the publication process. +</p> + +<p> + <em>This mail is automatically generated from the SciPost platform</em>. +</p> diff --git a/mails/templates/mail_templates/publication_ready.json b/mails/templates/mail_templates/publication_ready.json new file mode 100644 index 0000000000000000000000000000000000000000..fed59bff8e9546c56590e69f485cb82051b81119 --- /dev/null +++ b/mails/templates/mail_templates/publication_ready.json @@ -0,0 +1,5 @@ +{ + "subject": "SciPost: manuscript ready for publication", + "to_address": "admin@scipost.org", + "context_object": "publication" +} diff --git a/mails/templates/mails/mail_form.html b/mails/templates/mails/mail_form.html index 37150dbce30f320de9b8200f3af3ec7d15b6eacf..f1afc1a879536a84a3fe69de45cb7930ce025d19 100644 --- a/mails/templates/mails/mail_form.html +++ b/mails/templates/mails/mail_form.html @@ -15,7 +15,7 @@ {{ form|bootstrap }} <div class="form-group row"> <div class="offset-md-2 col-md-10"> - <input class="btn btn-secondary mr-2" type="reset" value="Reset to default"> + <input class="btn btn-outline-secondary mr-2" type="reset" value="Reset to default"> <button class="btn btn-primary mr-2" type="submit" name="save" value="send_from_editor">Send mail</button> </div> </div> diff --git a/mails/utils.py b/mails/utils.py index ee0d910d627e623f9c64064bb1894ce615aec123..f03191e05c44552b442dff0ad3c999c966cd4bb3 100644 --- a/mails/utils.py +++ b/mails/utils.py @@ -1,7 +1,7 @@ -from . import mixins +from .mixins import MailUtilsMixin -class DirectMailUtil(mixins.MailUtilsMixin): +class DirectMailUtil(MailUtilsMixin): """ Same templates and json files as the form EmailTemplateForm, but this will directly send the mails out, without intercepting and showing the mail editor to the user. diff --git a/mails/views.py b/mails/views.py index 61e4e3ccc9b79ca7e5221092335e3df649aa853c..51f68766cfd89afbaeaf25d94803fb2a094bda7a 100644 --- a/mails/views.py +++ b/mails/views.py @@ -26,3 +26,72 @@ class MailEditingSubView(object): def return_render(self): self.context['form'] = self.mail_form return render(self.request, self.template_name, self.context) + + +class MailEditorMixin: + """ + Use MailEditorMixin in edit CBVs to automatically implement the mail editor as + a post-form_valid hook. + + The view must specify the `mail_code` variable. + """ + object = None + mail_form = None + has_permission_to_send_mail = True + alternative_from_address = None # Tuple: ('from_name', 'from_address') + + def __init__(self, *args, **kwargs): + if not self.mail_code: + raise AttributeError(self.__class__.__name__ + ' object has no attribute `mail_code`') + super().__init__(*args, **kwargs) + + def get_template_names(self): + """ + The mail editor form has its own template. + """ + if self.mail_form and not self.mail_form.is_valid(): + return ['mails/mail_form.html'] + return super().get_template_names() + + def post(self, request, *args, **kwargs): + """ + Handle POST requests, but interpect the data if the mail form data isn't valid. + """ + if not self.has_permission_to_send_mail: + # Don't use the mail form; don't send out the mail. + return super().post(request, *args, **kwargs) + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + self.mail_form = EmailTemplateForm(request.POST or None, mail_code=self.mail_code, + instance=self.object) + if self.mail_form.is_valid(): + return self.form_valid(form) + + return self.render_to_response( + self.get_context_data(form=self.mail_form, + transfer_data_form=HiddenDataForm(form))) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """ + If both the regular form and mailing form are valid, save the form and run the mail form. + """ + # Don't use the mail form; don't send out the mail. + if not self.has_permission_to_send_mail: + return super().form_valid(form) + + if self.alternative_from_address: + # Set different from address if given. + self.mail_form.set_alternative_sender( + self.alternative_from_address[0], self.alternative_from_address[1]) + + response = super().form_valid(form) + try: + self.mail_form.send() + except AttributeError: + # self.mail_form is None + raise AttributeError('Did you check the order in which MailEditorMixin is used?') + messages.success(self.request, 'Mail sent') + return response diff --git a/news/factories.py b/news/factories.py index 74dcad74481d0c0de0e9dfeaabba153673a29723..a17229133a154ccd27117e135fac715a522ab54d 100644 --- a/news/factories.py +++ b/news/factories.py @@ -7,8 +7,8 @@ class NewsItemFactory(factory.django.DjangoModelFactory): class Meta: model = NewsItem - date = factory.Faker('date_time') - headline = factory.Faker('sentence', nb_words=6) - blurb = factory.Faker('text', max_nb_chars=200) - followup_link = factory.Faker('url') + date = factory.Faker('date_this_year') + headline = factory.Faker('sentence') + blurb = factory.Faker('paragraph', nb_sentences=8) + followup_link = factory.Faker('uri') followup_link_text = factory.Faker('sentence', nb_words=4) diff --git a/news/management/__init__.py b/news/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/news/management/commands/__init__.py b/news/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/news/management/commands/create_news.py b/news/management/commands/create_news.py new file mode 100644 index 0000000000000000000000000000000000000000..96e170762e48e6a2b57aa4e741786bff6b7247b2 --- /dev/null +++ b/news/management/commands/create_news.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from news import factories + + +class Command(BaseCommand): + help = 'Create random News Item objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of News items to add') + + def handle(self, *args, **kwargs): + self.create_news_items(kwargs['number']) + + def create_news_items(self, n): + factories.NewsItemFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} News Items.'.format(n=n))) diff --git a/notifications/models.py b/notifications/models.py index e05b53710ade32063ef292107119e6f9b0d4835c..f6dfb21ea25c0250531b17f3a074eb617904d1ac 100644 --- a/notifications/models.py +++ b/notifications/models.py @@ -3,7 +3,6 @@ from django.core.urlresolvers import reverse from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey -from django.utils import timezone from .constants import NOTIFICATION_TYPES from .managers import NotificationQuerySet diff --git a/notifications/templates/notifications/partials/notification_list_popover.html b/notifications/templates/notifications/partials/notification_list_popover.html new file mode 100644 index 0000000000000000000000000000000000000000..89f08094fdb87affd7f7d7f830c5695c988ffe0a --- /dev/null +++ b/notifications/templates/notifications/partials/notification_list_popover.html @@ -0,0 +1,52 @@ +{% load request_filters %} + +<div class="popover-template"> + <div class="popover notifications" role="tooltip"> + <div class="arrow"></div> + {% if user.contributor %} + <div class="header"> + <h3>{{ user.contributor.get_title_display }} {{ user.first_name }} {{ user.last_name }}</h3> + <a class="item" href="{% url 'scipost:update_personal_data' %}"><i class="fa fa-gear"></i> Update personal data</a> + </div> + {% if not user.contributor.is_currently_available %} + <div class="unavailable"> + <div class="head">You are currently unavailable</div> + <div class="text">Check your availability in your personal page if this should not be the case.</div> + </div> + {% endif %} + {% else %} + <div class="header"> + <h3>{{ user.first_name }} {{ user.last_name }}</h3> + </div> + {% endif %} + + <div class="links"> + <a class="item {% active 'scipost:personal_page' %}" href="{% url 'scipost:personal_page' %}">Personal Page</a> + {% if user.partner_contact or perms.scipost.can_read_partner_page %} + <a class="item {% active 'partners:dashboard' %}" href="{% url 'partners:dashboard' %}">Partner Page</a> + {% endif %} + + {% if perms.scipost.can_view_timesheets %} + <a class="item {% active 'finances:finance' %}" href="{% url 'finances:finance' %}">Financial Administration</a> + {% endif %} + + {% if perms.scipost.can_view_all_funding_info %} + <a class="item {% active 'funders:funders' %}" href="{% url 'funders:funders' %}">Funders</a> + {% endif %} + + {% if perms.scipost.can_view_production %} + <a class="item {% active 'production:production' %}" href="{% url 'production:production' %}">Production</a> + {% endif %} + + {% if perms.scipost.can_view_pool %} + <a class="item {% active 'submissions:pool' %}" href="{% url 'submissions:pool' %}">Submissions Pool</a> + {% endif %} + + <a class="item" href="{% url 'scipost:logout' %}">Logout</a> + </div> + + <h4 class="inbox-header">Inbox</h4> + <div class="live_notify_list"></div> + </div> + <div class="popover-body"></div> +</div> diff --git a/notifications/templatetags/notifications_tags.py b/notifications/templatetags/notifications_tags.py index 8d8bccd2402cd1f213000b4597ba82bc67390833..a89cafee93cf6d8501328fe71fb7958e3557c623 100644 --- a/notifications/templatetags/notifications_tags.py +++ b/notifications/templatetags/notifications_tags.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from django.core.urlresolvers import reverse from django.template import Library +from django.template.loader import render_to_string from django.utils.html import format_html register = Library() @@ -18,44 +18,12 @@ def live_notify_list(context): if not user: return '' - html = '<div class="popover-template popover">' - html += '<div class="popover notifications" role="tooltip">' - - # User default links - html += '<h6 class="header">Welcome {first_name} {last_name}</h6>'.format( - first_name=user.first_name, last_name=user.last_name) - - if hasattr(user, 'contributor'): - html += '<a class="item" href="{url}">Personal Page</a>'.format( - url=reverse('scipost:personal_page')) - - # User specific links - if user.has_perm('scipost.can_read_partner_page'): - html += '<a class="item" href="{url}">Partner Page</a>'.format( - url=reverse('partners:dashboard')) - if user.has_perm('scipost.can_view_timesheets'): - html += '<a class="item" href="{url}">Financial Administration</a>'.format( - url=reverse('finances:finance')) - if user.has_perm('scipost.can_view_all_funding_info'): - html += '<a class="item" href="{url}">Funders</a>'.format( - url=reverse('funders:funders')) - if user.has_perm('scipost.can_view_production'): - html += '<a class="item" href="{url}">Production</a>'.format( - url=reverse('production:production')) - if user.has_perm('scipost.can_view_pool'): - html += '<a class="item" href="{url}">Submission Pool</a>'.format( - url=reverse('submissions:pool')) - - # Logout links - html += '<div class="divider"></div>' - html += '<a class="item" href="{url}">Logout</a>'.format( - url=reverse('scipost:logout')) - - # Notifications - html += '<div class="divider"></div><h6 class="header">Inbox</h6>' - html += '<div class="live_notify_list"></div></div>' - html += '<div class="popover-body"></div></div>' - return format_html(html) + request = context['request'] + context = { + 'user': user, + } + return render_to_string('notifications/partials/notification_list_popover.html', + context, request=request) def user_context(context): diff --git a/notifications/views.py b/notifications/views.py index b36eaab4806253da91d5c18cfc1aa033983249b9..8728b683a0639b1230570b53d6139098809764f9 100644 --- a/notifications/views.py +++ b/notifications/views.py @@ -60,7 +60,7 @@ def live_notification_list(request): try: # Default to 5 as a max number of notifications - num_to_fetch = max(int(request.GET.get('max', 5)), 1) + num_to_fetch = max(int(request.GET.get('max', 10)), 1) num_to_fetch = min(num_to_fetch, 100) except ValueError: num_to_fetch = 5 diff --git a/package.json b/package.json index 9e3d508dcb107b4bbfa981744c683f1089a08a11..27c87b864b31e483f9dfb6fea3e702ab646d9d36 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "homepage": "https://www.scipost.org", "devDependencies": { "ajv": "^5.2.2", - "bootstrap": "^4.0.0-beta", + "bootstrap": "^4.0.0", "bootstrap-loader": "^2.1.0", "clean-webpack-plugin": "^0.1.15", "css-loader": "^0.28.4", @@ -28,10 +28,11 @@ "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^0.11.2", "imports-loader": "^0.7.1", - "jquery": "^2.2.0", + "jquery": "^3.3.1", + "jquery-ui": "^1.12.1", "node-loader": "^0.6.0", "node-sass": "^4.4.0", - "popper.js": "^1.11.1", + "popper.js": "^1.14.1", "postcss-load-config": "^1.2.0", "postcss-loader": "^2.0.6", "resolve-url-loader": "^1.6.1", diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html index 5ab2d85a1d11e9292d97d516f9fac472b24f7288..48c903504c317ae88d07ef63fdbd60c621bbd770 100644 --- a/partners/templates/partners/_prospective_partner_card.html +++ b/partners/templates/partners/_prospective_partner_card.html @@ -67,7 +67,7 @@ <form class="d-block mt-2 mb-3" action="{% url 'partners:add_prospartner_event' prospartner_id=pp.id %}" method="post"> {% csrf_token %} {{ ppevent_form|bootstrap }} - <input type="submit" name="submit" value="Submit" class="btn btn-secondary"> + <input type="submit" name="submit" value="Submit" class="btn btn-outline-secondary"> </form> <h3>Partner status</h3> diff --git a/partners/templates/partners/dashboard.html b/partners/templates/partners/dashboard.html index e99dd7b9629655d3c64698e8b5e4f03708525e7b..7ba9733e3ad8c27c4cb43afbdf5bd852e37aae87 100644 --- a/partners/templates/partners/dashboard.html +++ b/partners/templates/partners/dashboard.html @@ -20,20 +20,20 @@ <div class="tab-nav-inner"> <!-- Nav tabs --> <ul class="nav btn-group personal-page-nav" role="tablist"> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#account" class="nav-link active" data-toggle="tab">Account</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#agreements" class="nav-link" data-toggle="tab">Membership Agreements</a> </li> {% if perms.scipost.can_manage_SPB %} - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#prospartners" class="nav-link" data-toggle="tab">Prospective Partners</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#partners" class="nav-link" data-toggle="tab">Partners</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#global_agreements" class="nav-link" data-toggle="tab">Agreements</a> </li> {% endif %} diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html index 1637b4bfe06682c252a2b3e733a99d74db397785..7b75318c930ea9102b4bb31fe9ae7ab41656be08 100644 --- a/production/templates/production/partials/production_stream_card.html +++ b/production/templates/production/partials/production_stream_card.html @@ -14,7 +14,7 @@ <form action="{% url 'production:add_event' stream_id=stream.id %}" method="post" class="mb-2"> {% csrf_token %} {{ prodevent_form|bootstrap }} - <input type="submit" class="btn btn-secondary" name="submit" value="Submit"> + <input type="submit" class="btn btn-outline-secondary" name="submit" value="Submit"> </form> {% endif %} @@ -26,7 +26,7 @@ <form id="log_form" style="display: none;" action="{% url 'production:add_work_log' stream_id=stream.id %}" method="post" class="mb-2"> {% csrf_token %} {{ work_log_form|bootstrap }} - <input type="submit" class="btn btn-secondary" name="submit" value="Log"> + <input type="submit" class="btn btn-outline-secondary" name="submit" value="Log"> </form> </li> </ul> @@ -99,11 +99,11 @@ {% endif %} {% if perms.scipost.can_publish_accepted_submission %} - {% if not stream.submission.publication %} - <li><a href="{% url 'journals:initiate_publication' %}">Initiate the publication process</a></li> - {% endif %} <li><a href="{% url 'production:mark_as_completed' stream_id=stream.id %}">Mark this stream as completed</a></li> {% endif %} + {% if perms.scipost.can_draft_publication and stream.status == 'accepted' %} + <li><a href="{% url 'journals:update_publication' stream.submission.arxiv_identifier_w_vn_nr %}">Draft Publication</a></li> + {% endif %} </ul> {% endif %} {% endblock %} diff --git a/production/templates/production/partials/production_stream_card_completed.html b/production/templates/production/partials/production_stream_card_completed.html index 38f1c7fa637e5fafee9117caf225492c8dad3abb..555799b9e3d39eb0c5b124c9664f98351774fe0c 100644 --- a/production/templates/production/partials/production_stream_card_completed.html +++ b/production/templates/production/partials/production_stream_card_completed.html @@ -8,6 +8,9 @@ {% include 'partials/submissions/submission_card_content.html' with submission=stream.submission %} </div> <div class="card-body"> + {% if perms.scipost.can_draft_publication and stream.status == 'accepted' %} + <p>The proofs have been accepted. Please start <a href="{% url 'journals:update_publication' stream.submission.arxiv_identifier_w_vn_nr %}">drafting the Publication here</a>.</p> + {% endif %} <h3>Stream details</h3> <ul> <li>Status: <span class="label label-secondary label-sm">{{ stream.get_status_display }}</span></li> diff --git a/production/templates/production/production.html b/production/templates/production/production.html index 27d00bf8f8979bc4c7cde59ec6284c84f8ecedf2..4740ab7b5688055a0048b7b7420430cbf170996b 100644 --- a/production/templates/production/production.html +++ b/production/templates/production/production.html @@ -23,14 +23,14 @@ <div class="tab-nav-container"> <div class="tab-nav-inner"> <ul class="nav btn-group personal-page-nav" role="tablist"> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#streams" class="nav-link active" data-toggle="tab">{{ perms.scipost.can_assign_production_officer|yesno:"Streams,My Streams" }}</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#mytimesheet" class="nav-link" data-toggle="tab">My Timesheet</a> </li> {% if perms.scipost.can_promote_user_to_production_officer %} - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a href="#officers" class="nav-link" data-toggle="tab">Production Team</a> </li> {% endif %} diff --git a/production/templates/production/productionevent_form.html b/production/templates/production/productionevent_form.html index 854949c7c79d35ecc6bbe178227d58327091d369..5d89f7a75a031c69f612d0c22d519ef9ed66e831 100644 --- a/production/templates/production/productionevent_form.html +++ b/production/templates/production/productionevent_form.html @@ -20,7 +20,7 @@ <form method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" name="submit" value="Submit"> + <input type="submit" class="btn btn-outline-secondary" name="submit" value="Submit"> </form> </ul> </div> diff --git a/production/templates/production/upload_proofs.html b/production/templates/production/upload_proofs.html index 55efa68990e527c266748dbedc5dea085a9a4714..88289ff5915d0cdc623e60bc65d832182f1fa372 100644 --- a/production/templates/production/upload_proofs.html +++ b/production/templates/production/upload_proofs.html @@ -20,7 +20,7 @@ <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" name="submit" value="Upload"> + <input type="submit" class="btn btn-outline-secondary" name="submit" value="Upload"> </form> </ul> </div> diff --git a/requirements.txt b/requirements.txt index 21531ce520116fba8000b6c04f2201a7bb7ee953..1e0972f0744b34b251410d6032f2401f38763378 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,6 @@ django-webpack-loader==0.5 # Documentation Packages -alabaster==0.7.10 # Sphinx theme docutils==0.12 # What's this thing? Pygments==2.2.0 # Syntax highlighter Sphinx==1.4.9 @@ -32,9 +31,8 @@ sphinx-rtd-theme==0.1.9 # Sphinx theme # Testing -factory-boy==2.9.2 -Faker==0.7.18 -fake-factory==0.7.2 # Old version of Faker package +factory-boy==2.10.0 +Faker==0.8.12 # Django Utils diff --git a/scipost/behaviors.py b/scipost/behaviors.py index 7904262e934152b73b11ce7374c979ee55b02c1f..05c71495d0a34136cc02f3e18cf2c60b03a3c0ee 100644 --- a/scipost/behaviors.py +++ b/scipost/behaviors.py @@ -1,4 +1,5 @@ from django.db import models +from django.core.validators import RegexValidator from django.utils import timezone from .db.fields import AutoDateTimeField @@ -15,3 +16,7 @@ class TimeStampedModel(models.Model): class Meta: abstract = True + + +orcid_validator = RegexValidator(r'^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}$', + 'Please follow the ORCID format, e.g.: 0000-0001-2345-6789') diff --git a/scipost/factories.py b/scipost/factories.py index 67cb4e2df31821db68e780bbf44fef72d83636c2..0e3f561745d184e402192cf4de0c5d5a1f88ad28 100644 --- a/scipost/factories.py +++ b/scipost/factories.py @@ -4,31 +4,36 @@ import random from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from common.helpers import generate_orcid from submissions.models import Submission from .models import Contributor, EditorialCollege, EditorialCollegeFellowship, Remark from .constants import TITLE_CHOICES, SCIPOST_SUBJECT_AREAS -from django_countries.data import COUNTRIES -from faker import Faker - class ContributorFactory(factory.django.DjangoModelFactory): - title = random.choice(list(dict(TITLE_CHOICES).keys())) + title = factory.Iterator(TITLE_CHOICES, getter=lambda c: c[0]) user = factory.SubFactory('scipost.factories.UserFactory', contributor=None) status = 1 # normal user - vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None) - personalwebpage = factory.Faker('url') - country_of_employment = factory.Iterator(list(COUNTRIES)) - affiliation = factory.Faker('company') + vetted_by = factory.Iterator(Contributor.objects.all()) + personalwebpage = factory.Faker('uri') expertises = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: [c[0]]) - personalwebpage = factory.Faker('domain_name') + orcid_id = factory.lazy_attribute(lambda n: generate_orcid()) address = factory.Faker('address') + invitation_key = factory.Faker('md5') + activation_key = factory.Faker('md5') + key_expires = factory.Faker('future_datetime') class Meta: model = Contributor django_get_or_create = ('user',) + @factory.post_generation + def add_to_vetting_editors(self, create, extracted, **kwargs): + if create: + from affiliations.factories import AffiliationFactory + AffiliationFactory(contributor=self) + class VettingEditorFactory(ContributorFactory): @factory.post_generation @@ -69,7 +74,7 @@ class EditorialCollegeFactory(factory.django.DjangoModelFactory): class Meta: model = EditorialCollege - django_get_or_create = ('discipline', ) + django_get_or_create = ('discipline',) class EditorialCollegeFellowshipFactory(factory.django.DjangoModelFactory): @@ -85,7 +90,7 @@ class SubmissionRemarkFactory(factory.django.DjangoModelFactory): contributor = factory.Iterator(Contributor.objects.all()) submission = factory.Iterator(Submission.objects.all()) date = factory.Faker('date_time_this_decade') - remark = factory.lazy_attribute(lambda x: Faker().paragraph()) + remark = factory.Faker('paragraph') class Meta: model = Remark diff --git a/scipost/feeds.py b/scipost/feeds.py index 738a1c9567a60b8004ad3fba2f543edec9056c6e..e58758d2fe56a7b4aa24b1e0083541e757515824 100644 --- a/scipost/feeds.py +++ b/scipost/feeds.py @@ -1,6 +1,7 @@ import datetime from django.contrib.syndication.views import Feed +from django.http import Http404 from django.utils.feedgenerator import Atom1Feed from django.core.urlresolvers import reverse from django.db.models import Q @@ -34,7 +35,7 @@ class LatestCommentsFeedRSS(Feed): elif item.submission: return reverse('submissions:submission', kwargs={'arxiv_identifier_w_vn_nr': - item.submission.arxiv_identifier_w_vn_nr,}) + item.submission.arxiv_identifier_w_vn_nr}) elif item.thesislink: return reverse('theses:thesis', kwargs={'thesislink_id': item.thesislink.id}) @@ -142,27 +143,26 @@ class LatestPublicationsFeedRSS(Feed): link = "/journals/" def get_object(self, request, subject_area=''): - if subject_area != '': - queryset = Publication.objects.filter( - Q(subject_area=subject_area) | Q(secondary_areas__contains=[subject_area]) - ).order_by('-publication_date')[:10] - queryset.subject_area = subject_area - else: - queryset = Publication.objects.order_by('-publication_date')[:10] - queryset.subject_area = None - return queryset + if subject_area and subject_area not in subject_areas_dict: + raise Http404('Invalid subject area') + qs = Publication.objects.published() + if subject_area: + qs = qs.filter( + Q(subject_area=subject_area) | Q(secondary_areas__contains=[subject_area])) + self.subject_area = subject_area + return qs.order_by('-publication_date')[:10] def title(self, obj): title_text = 'SciPost: Latest Publications' - if obj.subject_area: - title_text += ' in %s' % subject_areas_dict[obj.subject_area] + if self.subject_area: + title_text += ' in %s' % subject_areas_dict.get(self.subject_area) return title_text def description(self, obj): desc = 'SciPost: most recent publications' try: - if obj.subject_area: - desc += ' in %s' % subject_areas_dict[obj.subject_area] + if self.subject_area: + desc += ' in %s' % subject_areas_dict.get(self.subject_area) except KeyError: pass return desc diff --git a/scipost/forms.py b/scipost/forms.py index 9f68b07836b683bf59804cdf3fb3e37647dd66d0..d76f21a126892a8cb30eb0d22b81afa74c5a2f6e 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -19,6 +19,7 @@ from captcha.fields import ReCaptchaField from ajax_select.fields import AutoCompleteSelectField from haystack.forms import ModelSearchForm as HayStackSearchForm +from .behaviors import orcid_validator from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES from .decorators import has_contributor from .models import Contributor, DraftInvitation,\ @@ -42,6 +43,27 @@ REGISTRATION_REFUSAL_CHOICES = ( reg_ref_dict = dict(REGISTRATION_REFUSAL_CHOICES) +class RequestFormMixin: + """ + This mixin lets the Form accept `request` as an argument. + """ + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request') + super().__init__(*args, **kwargs) + + +class HttpRefererFormMixin(RequestFormMixin): + """ + This mixin adds a HiddenInput to the form which tracks the previous url, which can + be used to redirect to. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['http_referer'] = forms.URLField(widget=forms.HiddenInput(), required=False) + if self.request: + self.fields['http_referer'].initial = self.request.META.get('HTTP_REFERER') + + class RegistrationForm(forms.Form): """ Use this form to process the registration of new accounts. @@ -55,6 +77,7 @@ class RegistrationForm(forms.Form): email = forms.EmailField(label='* Email address') invitation_key = forms.CharField(max_length=40, widget=forms.HiddenInput(), required=False) orcid_id = forms.CharField(label="ORCID id", max_length=20, required=False, + validators=[orcid_validator], widget=forms.TextInput( {'placeholder': 'Recommended. Get one at orcid.org'})) discipline = forms.ChoiceField(choices=SCIPOST_DISCIPLINES, label='* Main discipline') diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 9a85a5165eb7b6cec44d41b2af231ea5701e3ef2..aa4d6d5ce525bbbdf5dcef55708ae01cedae68a3 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -248,6 +248,14 @@ class Command(BaseCommand): codename='can_view_all_funding_info', name='Can view all Funders info', content_type=content_type) + can_create_grants, created = Permission.objects.get_or_create( + codename='can_create_grants', + name='Can create Grant', + content_type=content_type) + can_draft_publication, created = Permission.objects.get_or_create( + codename='can_draft_publication', + name='Can draft Publication', + content_type=content_type) # Documentation can_view_docs_scipost, created = Permission.objects.get_or_create( @@ -325,7 +333,9 @@ class Command(BaseCommand): can_view_production, can_view_timesheets, can_publish_accepted_submission, + can_draft_publication, can_view_all_funding_info, + can_create_grants, can_attend_VGMs, can_manage_reports, can_assign_production_supervisor, @@ -376,6 +386,8 @@ class Command(BaseCommand): ProductionSupervisors.permissions.set([ can_assign_production_officer, can_take_decisions_related_to_proofs, + # can_draft_publication, + # can_create_grants, can_view_all_production_streams, can_run_proofs_by_authors, can_view_docs_scipost, diff --git a/scipost/management/commands/create_contributors.py b/scipost/management/commands/create_contributors.py new file mode 100644 index 0000000000000000000000000000000000000000..025513c74d49b6f82a77268706cf84d314a950bb --- /dev/null +++ b/scipost/management/commands/create_contributors.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from scipost import factories + + +class Command(BaseCommand): + help = 'Create random Contributor objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Contributors to add') + + def handle(self, *args, **kwargs): + self.create_contributors(kwargs['number']) + + def create_contributors(self, n): + factories.ContributorFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Contributors.'.format(n=n))) diff --git a/scipost/management/commands/create_remarks.py b/scipost/management/commands/create_remarks.py new file mode 100644 index 0000000000000000000000000000000000000000..19ebe3e92c5098192ae4880b149f5822018528f5 --- /dev/null +++ b/scipost/management/commands/create_remarks.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from scipost import factories + + +class Command(BaseCommand): + help = 'Create random Remark objects (related to a Submission) using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Remarks to add') + + def handle(self, *args, **kwargs): + self.create_remarks(kwargs['number']) + + def create_remarks(self, n): + factories.SubmissionRemarkFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Remarks.'.format(n=n))) diff --git a/scipost/management/commands/populate_db.py b/scipost/management/commands/populate_db.py index 4efe19d85887e5a79782a2b8defb1919286e049f..4812d71e6fa2958a98c7792ff5c97579aa94b83e 100644 --- a/scipost/management/commands/populate_db.py +++ b/scipost/management/commands/populate_db.py @@ -1,34 +1,11 @@ from django.core.management.base import BaseCommand -from commentaries.factories import VettedCommentaryFactory -from comments.factories import CommentaryCommentFactory, SubmissionCommentFactory,\ - ThesislinkCommentFactory -from scipost.factories import SubmissionRemarkFactory -from journals.factories import JournalFactory, VolumeFactory, IssueFactory, PublicationFactory -from news.factories import NewsItemFactory -from submissions.factories import EICassignedSubmissionFactory -from theses.factories import VettedThesisLinkFactory - -from ...factories import ContributorFactory, EditorialCollegeFactory,\ - EditorialCollegeFellowshipFactory +from comments.factories import CommentaryCommentFactory,\ + ThesislinkCommentFactory, ReplyCommentFactory class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument( - '--news', - action='store_true', - dest='news', - default=False, - help='Add NewsItems', - ) - parser.add_argument( - '--commentaries', - action='store_true', - dest='commentaries', - default=False, - help='Add 5 Commentaries', - ) parser.add_argument( '--comments', action='store_true', @@ -36,147 +13,13 @@ class Command(BaseCommand): default=False, help='Add 10 Comments', ) - parser.add_argument( - '--contributor', - action='store_true', - dest='contributor', - default=False, - help='Add 5 Contributors', - ) - parser.add_argument( - '--college', - action='store_true', - dest='editorial-college', - default=False, - help='Add 5 Editorial College and Fellows (Contributors required)', - ) - parser.add_argument( - '--pubset', - action='store_true', - dest='pubset', - default=False, - help='Add 5 Issues, Volumes and Journals', - ) - parser.add_argument( - '--issues', - action='store_true', - dest='issues', - default=False, - help='Add 5 Issues', - ) - parser.add_argument( - '--submissions', - action='store_true', - dest='submissions', - default=False, - help='Add 5 new submissions status EIC assigned', - ) - parser.add_argument( - '--publications', - action='store_true', - dest='publications', - default=False, - help='Add 5 Publications (includes --issues action)', - ) - parser.add_argument( - '--remarks', - action='store_true', - dest='remarks', - default=False, - help='Add 5 new Remarks linked to Submissions', - ) - parser.add_argument( - '--theses', - action='store_true', - dest='theses', - default=False, - help='Add 5 ThesisLinks', - ) - parser.add_argument( - '--all', - action='store_true', - dest='all', - default=False, - help='Add all available', - ) def handle(self, *args, **kwargs): - if kwargs['contributor'] or kwargs['all']: - n = 5 - if kwargs['all']: - n += 10 - self.create_contributors(n) - if kwargs['commentaries'] or kwargs['all']: - self.create_commentaries() - if kwargs['comments'] or kwargs['all']: + if kwargs['comments']: self.create_comments() - if kwargs['editorial-college'] or kwargs['all']: - self.create_editorial_college() - self.create_editorial_college_fellows() - if kwargs['news'] or kwargs['all']: - self.create_news_items() - if kwargs['submissions'] or kwargs['all']: - self.create_submissions() - if kwargs['pubset'] or kwargs['all']: - self.create_pubset() - if kwargs['issues'] or kwargs['all']: - self.create_issues() - if kwargs['publications'] or kwargs['all']: - self.create_publications() - if kwargs['remarks'] or kwargs['all']: - self.create_remarks() - if kwargs['theses'] or kwargs['all']: - self.create_theses() - - def create_contributors(self, n=5): - ContributorFactory.create_batch(n) - self.stdout.write(self.style.SUCCESS('Successfully created %i Contributors.' % n)) - - def create_commentaries(self): - VettedCommentaryFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Commentaries.')) def create_comments(self): CommentaryCommentFactory.create_batch(3) - SubmissionCommentFactory.create_batch(4) + ReplyCommentFactory.create_batch(2) ThesislinkCommentFactory.create_batch(3) self.stdout.write(self.style.SUCCESS('Successfully created 10 Comments.')) - - def create_editorial_college(self): - EditorialCollegeFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Editorial College\'s.')) - - def create_editorial_college_fellows(self): - EditorialCollegeFellowshipFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Editorial College Fellows.')) - - def create_news_items(self): - NewsItemFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 News items.')) - - def create_submissions(self): - EICassignedSubmissionFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Submissions.')) - - def create_pubset(self): - VolumeFactory.create_batch(5) - IssueFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS( - 'Successfully created 5x {Journal, Volume and Issue}.')) - - def create_issues(self): - IssueFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS( - 'Successfully created 5 Issue.')) - - def create_publications(self): - PublicationFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Publications.')) - - def create_remarks(self): - SubmissionRemarkFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 Remarks.')) - - def create_theses(self): - VettedThesisLinkFactory.create_batch(5) - self.stdout.write(self.style.SUCCESS('Successfully created 5 ThesisLinks.')) diff --git a/scipost/migrations/0007_auto_20180314_1502.py b/scipost/migrations/0007_auto_20180314_1502.py new file mode 100644 index 0000000000000000000000000000000000000000..6f5d6e3e9487a5660baf324f47590992396fcddc --- /dev/null +++ b/scipost/migrations/0007_auto_20180314_1502.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 14:02 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0006_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='orcid_id', + field=models.CharField(blank=True, max_length=20, validators=[django.core.validators.RegexValidator('^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}$', 'Please follow the ORCID format, e.g.: 0000-0001-2345-6789')], verbose_name='ORCID id'), + ), + ] diff --git a/scipost/migrations/0007_auto_20180318_1336.py b/scipost/migrations/0007_auto_20180318_1336.py new file mode 100644 index 0000000000000000000000000000000000000000..26897d9aea5bd5ded92719f9da74421d07ec4cbe --- /dev/null +++ b/scipost/migrations/0007_auto_20180318_1336.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-18 12:36 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0006_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='contributor', + name='orcid_id', + field=models.CharField(blank=True, max_length=20, validators=[django.core.validators.RegexValidator('^[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{4}$', 'Please follow the ORCID format, e.g.: 0000-0001-2345-6789')], verbose_name='ORCID id'), + ), + ] diff --git a/scipost/migrations/0008_merge_20180322_1031.py b/scipost/migrations/0008_merge_20180322_1031.py new file mode 100644 index 0000000000000000000000000000000000000000..941ff01d148f5afa4e40a7d1156cc2f99cf3d668 --- /dev/null +++ b/scipost/migrations/0008_merge_20180322_1031.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-22 09:31 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0007_auto_20180318_1336'), + ('scipost', '0007_auto_20180314_1502'), + ] + + operations = [ + ] diff --git a/scipost/mixins.py b/scipost/mixins.py index 0cdb24844cca643934797c665b3357a01caabf82..b7d76dc854dfffc2ed458f0cc0b49ace0d2a66fd 100644 --- a/scipost/mixins.py +++ b/scipost/mixins.py @@ -1,15 +1,21 @@ +from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin + from .paginator import SciPostPaginator +class PermissionsMixin(LoginRequiredMixin, PermissionRequiredMixin): + pass + + class PaginationMixin: """ Mixin for generic class-based views (e.g. django.views.generic.ListView) """ paginator_class = SciPostPaginator - # def get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True): - # # Pass the request object to the paginator to keep the parameters in the - # # url querystring ("?page=2&old_param=...") - # request = self.request - # return self.paginator_class(queryset, per_page, orphans=orphans, - # allow_empty_first_page=allow_empty_first_page, request=request) + +class RequestViewMixin: + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs diff --git a/scipost/models.py b/scipost/models.py index 46e448359e087de28e4b7c138255143d56f45354..8b101183f9ce70eaef82050ed0f46b6be5ce2ff0 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -10,7 +10,7 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils import timezone -from .behaviors import TimeStampedModel +from .behaviors import TimeStampedModel, orcid_validator from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ subject_areas_dict, CONTRIBUTOR_STATUS, TITLE_CHOICES,\ INVITATION_STYLE, INVITATION_TYPE,\ @@ -52,7 +52,7 @@ class Contributor(models.Model): models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS), blank=True, null=True) orcid_id = models.CharField(max_length=20, verbose_name="ORCID id", - blank=True) + blank=True, validators=[orcid_validator]) address = models.CharField(max_length=1000, verbose_name="address", blank=True) personalwebpage = models.URLField(verbose_name='personal web page', diff --git a/scipost/static/scipost/SciPost.css b/scipost/static/scipost/SciPost.css index 44e1b4c27a9f5f124ba0bfed164a4ffca84c4685..af05481c254ebc51118cd25efa8bed3022dc0729 100644 --- a/scipost/static/scipost/SciPost.css +++ b/scipost/static/scipost/SciPost.css @@ -4,321 +4,12 @@ General style sheet for SciPost @import url(//fonts.googleapis.com/css?family=Merriweather+Sans); - -hr.hr6 { - height: 6px; - border: 0; - box-shadow: inset 0 6px 6px -6px rgba(0, 0, 0, 0.5); -} -hr.hr12 { - height: 12px; - border: 0; - box-shadow: inset 0 12px 12px -12px rgba(0, 0, 0, 0.5); -} - - -ul.personalTabMenu { - background-color: #dddddd; - display: inline-block; - font-size: 11px; - padding: 0px; -} -ul.personalTabMenu li { - display: inline-block; - margin: 0px; - padding: 4px; -} -ul.personalTabMenu li a { - padding: 4px; -} -ul.personalTabMenu li a.active { - background-color: #eeeeee; - border: 1px solid black; - padding: 4px; -} -ul.personalTabMenu li a.inactive { - background-color: #dddddd; - border: none; -} - -.assignments_listing { - border-collapse: collapse; - margin: 0px 5px; - padding: 0px; -} -.assignments_listing tr { - background-color: #dddddd; - border: 1px solid black; -} -.assignments_listing td { - border: 1px solid black; - padding: 4px 8px; -} -.assignments_listing th { - border: 1px solid black; - padding: 4px 8px; -} - -.commentcategorychoices { - list-style-type: none; -} -.commentcategorydisplay { - display: inline-block; - font-size: 10px; - margin: 2px 2px; - padding: 1px; - box-shadow: 1px 1px 3px #888888; - background: linear-gradient(to right,#fcfcfc, #f0f0f0); -} -.commentcategorydisplay h4, .commentcategorydisplay ul { - display: inline-block; - margin: 2px; - padding: 0px; -} -.commentcategorydisplay li { - display: inline-block; - margin: 2px; - padding: 4px 8px; - background-color: #dddddd; - font-size: 90%; -} - -.commentid { - /*display: inline-block;*/ -/* box-shadow: 1px 1px 3px #888888;*/ -/* background: linear-gradient(to right,#f8f8f8, #e0e0e0);*/ - /*font-size: 10px;*/ -} - -.forTesters { - background-color: #eeeeff; -} - -.AddItemToList { - background-color: #000099; - border: 1px; - color: white; - padding: 4px; - font-size: 50%; -} -.RemoveItemFromList { - background-color: #990000; - border: 1px; - color: white; - padding: 4px; - font-size: 50%; -} - -.GraphButton { - background-color: #dddddd; - font-size: 50%; - padding: 4px; -} -.GraphButton h1 { - padding: 1px; -} - -.reportid { - display: inline-block; - box-shadow: 5px 5px 10px #888888; - background: linear-gradient(to right,#f8f8f8, #e0e0e0); -} - -.reportingDeadline { - color: #ff0000; -} - -/* For two-column text display */ -.twocolumn { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - -webkit-column-width: 360px; /* Chrome, Safari, Opera */ - column-width: 360px; - -webkit-column-rule-width: 1px; /* Chrome, Safari, Opera */ - -moz-column-rule-width: 1px; /* Firefox */ - column-rule-width: 1px; -} - -.flex-container { - display: -webkit-flex; - display: flex; - flex-wrap: wrap; -} -.flex-container #headerflex { - align-contents: center; - justify-content: space-between; - margin: 0px; - padding: 0px; -} -.flex-logobox { - align-self: center; - margin: 0px; - padding: 0px; -} -.flex-blurbbox { - margin: 0px; - padding: 0px; - align-self: center; - margin-left: auto; -} -.flex-commentbox { -/* background-color: #f4f4f4; */ - margin: 0px; - padding: 0px; - box-shadow: 1px 1px 3px #888888; - background: linear-gradient(to right,#f8f8f8, #e0e0e0); -} -.flex-Fellowactionbox { -/* background-color: #6884C2;*/ - background-color: #cccccc; - margin: 0px; - padding: 5px 10px; -} -.flex-greybox { - background-color: #f4f4f4; - margin: 10px; - padding: 10px; -} -.flex-greybox320 { - background-color: #f4f4f4; - width: 320px; - margin: 10px; - padding: 10px; -} -.flex-greybox480 { - background-color: #f4f4f4; - width: 480px; - margin: 10px; - padding: 10px; -} -.flex-greybox640 { - background-color: #f4f4f4; - width: 640px; - margin: 10px; - padding: 10px; -} -.flex-greybox800 { - background-color: #f4f4f4; - width: 800px; - margin: 10px; - padding: 10px; -} -.flex-whitebox { - margin: 10px; - padding: 10px; -} -.flex-whitebox320 { - width: 320px; - margin: 10px; - padding: 10px; -} -.flex-whitebox480 { - width: 480px; - margin: 10px; - padding: 10px; -} -.flex-whitebox640 { - width: 640px; - margin: 10px; - padding: 10px; -} -.flex-whitebox800 { - width: 800px; - margin: 10px; - padding: 10px; -} -.flex-whitebox0 { - margin: 0px; - padding: 0px; -} -.flex-whitebox0v { - margin: 0px 10px; - padding: 0px; -} -.flex-thesesbox { - width: 480px; - margin: 0px 10px; - padding: 0px; -} - .pubtitleli { - martin: 1px; padding: 3px 5px; background-color: #f4f4f4; border-radius: 4px; } -.ref_listing { - border-collapse: collapse; - margin: 0px 5px; - padding: 0px; -} -.ref_listing tr { - background-color: #dddddd; - border: 1px solid black; -} -.ref_listing td { - padding: 2px 8px; -} - -.tableofInvitees td { - padding: 0px 4px; -} -.tableofInviteesResponded td { - background-color: #f0f0f0; - padding: 0px 4px; -} -.tableofInviteesDeclined td { - background-color: #e09090; - padding: 0px 4px; -} - -body section div.sectionbox { - width: 300px; - background-color: #f4f4f4; - margin: 10px; - padding: 10px; - display: inline-block; - vertical-align: top; -} - -.submitButton { - background-color: #002B49; - color: #ffffff; - border: 1px solid #FFA300; - margin: 5px; - padding: 5px; -} - -header { - background-color: #002B49; - color: #ffffff; - border: 0px solid grey; border-radius:0px; - margin: 0px; - padding: 0px 5px; -} -header h1 { - font-size: 50px; - font-weight: 400; - margin: 10px; - padding: 10px; -} -header a { - text-decoration: none; -} -header a:hover { - color: #ffffff; -} -header img { - margin: 10px 20px; - padding: 0px; -} -header p { - border: 0px solid white; - margin: 25px; -} - ol { list-style-type: none; counter-reset: item; @@ -343,89 +34,9 @@ li ol > li:before { } -p#goodCommenter { +.good_commenter { font-size: 90%; -} - -section { - color: #111111; - background-color: white; - margin: 0px 15px; - padding: 10px; - text-align: justify; -} -section a { - color: #6884C2; - text-decoration: none; - target: _parent; -} -section a:hover { - color: #103050; - text-decoration: underline; -} -section div { - margin: 0px; - padding: 0px; -} - -section h1, h2, h3, h4, h5, h6 { - margin: 0px; - padding: 6px 2px; -/* color: #103050; */ - text-shadow: 0px 0px 0px #222222; - font-weight: 500; -} -section form { - margin: 0px; - padding: 0px; -} -section form label { - color: black; - font-weight: 400; -} -section form ul li { - padding: 0px; -} -section form ul li label { - color: black; -} - -form#refereeSearch input#id_last_name{ - width: 20em; - margin: 1em; -} - - -section p { - margin: 2px; - padding: 2px; -} -section table { - margin: 0px; - padding: 0px; -} -section table th, td { - color: #102040; - text-align: left; - padding: 0px; -} - -section ul { - margin: 0px; -} -section ul li { - padding: 2px; -} - -table.tablePadded10 th { - padding: 5px 10px; -} -table.tablePadded10 td { - padding: 0px 10px; -} - -#NWOOpenAccess { - font-size: 80%; + font-style: italic; } .opinionsDisplay { @@ -486,178 +97,7 @@ table.tablePadded10 td { background-color: #990000; } -article { - background-color:#eeeeee; - border: 1px solid grey; border-radius:5px; - margin:5px; margin-bottom:10px; - padding:5px; -} - -footer { - background-color: #002B49; - color: #ffffff; - border-top: 1px solid grey; -} -footer a { - color: #eeeeee; -} -footer a:hover { - color: #ffffff; - text-decoration: underline; -} - -input, textarea { - background-color: #f8f8f8; - color: black; - border: 1px solid black; border-radius: 1px; -} -/*input[type='text'], input[type='URL'], textarea { - max-width: 800px; - min-width: 200px; -}*/ -input:hover { - text-decoration: underline; -} - -select { - background-color: #f8f8f8; - color: black; - border: 1px solid black; -} -@keyframes journalsannounceanim { - 0% { background-color: #110000; } - 50% { background-color: #aa0000; } - 100% { background-color: #110000; } -} - -#journalsannouncesmall { -/* background-color: #880000; */ - background-color: #FFDDDD; - color: #FF0000; - padding: 10px; - font-size: 90%; - text-align: center; -/* animation-name: journalsannounceanim; */ -/* animation-duration: 2s; */ -/* animation-iteration-count: infinite; */ -} -#journalsannounce { -/* background-color: #880000; */ - background-color: #100000; - color: #FF0000; - padding: 20px; - font-size: 150%; - text-align: center; -/* animation-name: journalsannounceanim; */ -/* animation-duration: 2s; */ -/* animation-iteration-count: infinite; */ -} - - -ul.NewsItemsList { - list-style-type: none; - margin: 0px; - padding: 0px; -} -/*ul.NewsItemsList li { - margin: 10px; -}*/ -h3.NewsHeadline { - background-color: #002B49; - color: #C3D7EE; - padding: 10px; -} - -.SciPostPhysicsTab { - background-color: #002B49; - color: #FFA300; - margin: 2px; - padding: 5px 10px; - font-size: 120%; - text-align: center; -} -.SciPostPhysicsTab h3 a { - color: #FFA300; -} - -ul.SciPostPhysicsTabMenu { - background-color: #dddddd; - display: block; - font-size: 16px; - padding: 0px; -} -ul.SciPostPhysicsTabMenu li { - display: inline-block; - margin: 0px; - padding: 10px; -} -ul.SciPostPhysicsTabMenu li a { - padding: 4px; -} -ul.SciPostPhysicsTabMenu li a.active { - background-color: #eeeeee; - border: 1px solid black; - padding: 4px; -} -ul.SciPostPhysicsTabMenu li a.inactive { - background-color: #dddddd; - border: none; -} - -ul.publicationHeaderList { - list-style-type: none; - margin: 0px; - padding: 0px; -} - -li.publicationHeader { - background-color: #fafafa; - margin: 10px; - padding: 0px; -} - -p.publicationTitle { - background-color: #eeeeee; - font-size: 16px; - margin: 0px; - padding: 10px; -} - -p.publicationAuthors { - font-weight: bold; -} - - -/* For modal boxes (e.g. BiBTeX) */ -.modalDialog { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(0,0,0,0.5); - z-index: 99999; - opacity: 0; - -webkit-transition: opacity 200ms ease-in; - -moz-transition: opacity 200ms ease-in; - transition: opacity 200ms ease-in; - pointer-events: none; -} - -.modalDialog:target { - opacity: 1; - pointer-events: auto; -} - -.modalDialog > div { - width: 600px; - position: relative; - margin: 10% auto; - padding: 10px; - border-radius: 10px; - background: #ffffff; -} #preview-comment_text { border: 1px solid black; @@ -666,22 +106,6 @@ p.publicationAuthors { padding: 1rem; border-radius: 1.4px; } -#preview-strengths { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-weaknesses { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-report { - border: 1px solid black; - white-space: pre-wrap; -} -#preview-requested_changes { - border: 1px solid black; - white-space: pre-wrap; -} /* Styling of sphinxdoc-generated docs */ .pagination-top { @@ -719,18 +143,3 @@ p.publicationAuthors { visibility: hidden; } .sphinx *:hover > a.headerlink { visibility: visible; } - -.pagination-bottom { - background-color: #dddddd; - color: black; - margin: 10px; - padding: 10px; -} -.pagination-bottom a { - color: #666666; - target: _parent; -} -.pagination-bottom a:hover { - color: #aaaaaa; - text-decoration: underline; -} diff --git a/scipost/static/scipost/assets/config/preconfig.scss b/scipost/static/scipost/assets/config/preconfig.scss index a9b5266ee232496003ad1e192a0c62ba668a0b79..95228df4dc669a64279ee467a18269b6fe957322 100644 --- a/scipost/static/scipost/assets/config/preconfig.scss +++ b/scipost/static/scipost/assets/config/preconfig.scss @@ -34,17 +34,13 @@ $white: #fff; $blue: $scipost-lightblue; // Primary $green: #6ebb6e; $cyan: $scipost-lightestblue; +$orange: $scipost-orange; $yellow: $scipost-orange; $gray-100: $scipost-white; $gray-200: #e5e5e5; $gray-300: #dfdfdf; $gray-600: #ccc; $text-muted: #636c72; -// Others, now as default -// $red: $red; -// $gray-100: $gray-100; -// $gray-600: $gray-600; // Secondary -// $gray-800: $gray-800; // Body // @@ -79,8 +75,8 @@ $breadcrumb-divider-color: $scipost-orange; $input-btn-padding-y-sm: 0.1rem; $input-btn-padding-x: 0.5rem; $input-btn-padding-y: 0.25rem; -$input-btn-padding-y-lg: 0.25rem; $input-btn-padding-x-lg: 0.75rem; +$input-btn-padding-y-lg: 0.325rem; $input-btn-line-height-lg: 1.4; $input-border-radius-sm: $base-border-radius; $input-border-radius: $base-border-radius; @@ -110,15 +106,15 @@ $font-family-monospace: $font-family-sans-serif; $font-family-base: $font-family-sans-serif; $font-size-base: 0.8rem; -$font-size-sm: 0.75rem; +$font-size-sm: 0.7rem; $font-size-lg: 1.0rem; $h1-font-size: 1.8em; $h2-font-size: 1.5em; -$h3-font-size: 1.25em; -$h4-font-size: 0.8rem; -$h5-font-size: 0.8rem; -$h6-font-size: 0.8rem; +$h3-font-size: 1.3em; +$h4-font-size: 1.2em; +$h5-font-size: 1.0em; +$h6-font-size: 1.0em; $close-font-weight: 100; @@ -127,13 +123,16 @@ $close-font-weight: 100; $table-cell-padding: 0.25rem 0.5rem; +// Tooltip +// +$tooltip-font-size: $font-size-sm; + // Navbar // $navbar-light-color: $scipost-darkblue; $navbar-light-hover-color: $scipost-darkblue; $navbar-padding-x: 0.3rem; -$navbar-padding-y: 0.3rem; -// $nav-link-padding-y: 0.4rem; +$navbar-padding-y: 0.35rem; $input-border-radius: 0; $btn-border-radius: $base-border-radius; @@ -144,19 +143,17 @@ $btn-border-radius-lg: $base-border-radius; // Block quote // $blockquote-font-size: $font-size-base; -// $blockquote-border-color: #ececec; // Popover // -$popover-border-color: #dddddd; -$popover-arrow-width: 5px; -$popover-arrow-height: 5px; -// $popover-arrow-color: $popover-bg; -$popover-max-width: 500px; - -$popover-arrow-outer-width: ($popover-arrow-width + 1px); - +$popover-font-size: $font-size-sm; +$popover-border-color: $gray-600; +$popover-arrow-width: 20px; +$popover-arrow-height: 10px; +$popover-arrow-color: $white; +$popover-arrow-outer-color: $gray-600; +$popover-max-width: 500px; // --- diff --git a/scipost/static/scipost/assets/css/_breadcrumb.scss b/scipost/static/scipost/assets/css/_breadcrumb.scss index 633e86b5918e2f684f9b680841de16114693fd09..5cc416301fa869a9cc88185de1c647211e8e5360 100644 --- a/scipost/static/scipost/assets/css/_breadcrumb.scss +++ b/scipost/static/scipost/assets/css/_breadcrumb.scss @@ -11,6 +11,8 @@ white-space: nowrap; overflow: auto; padding: 0.5rem 1.0rem; + display: flex; + flex-direction: row; .breadcrumb-item { margin-bottom: 0; diff --git a/scipost/static/scipost/assets/css/_buttons.scss b/scipost/static/scipost/assets/css/_buttons.scss index 7a7b57a4ec67b3e60f7432b80d8e0bebc8b3362d..8f09c54442f83ad73158103b72cb4122963d57c3 100644 --- a/scipost/static/scipost/assets/css/_buttons.scss +++ b/scipost/static/scipost/assets/css/_buttons.scss @@ -2,41 +2,13 @@ * Buttons * */ -.btn { - cursor: pointer; - font-family: inherit; - - &:focus, - &.focus { - box-shadow: 0 0 0 3px rgba(104, 133, 195, 0.3); - } -} - .btn-link { color: $scipost-lightblue; } -.btn-secondary { +.btn-outline-secondary { color: $scipost-darkblue; background-color: $white; - border-color: $gray-600; - - &:hover, - &.hover { - color: $scipost-darkblue; - background-color: #ddd; - border-color: $gray-600; - } - - &:active, - &.active { - background-color: #d4d4d4; - border-color: $gray-600; - } -} - -.btn-info { - color: $scipost-darkblue; } .category-group, diff --git a/scipost/static/scipost/assets/css/_cards.scss b/scipost/static/scipost/assets/css/_cards.scss index ad7c1357f09bb4c59636abe1fe028532f06e667a..6e1e57b63d1eba55342a323af040498dd0d65d1e 100644 --- a/scipost/static/scipost/assets/css/_cards.scss +++ b/scipost/static/scipost/assets/css/_cards.scss @@ -4,7 +4,7 @@ &.card-grey { background-color: $card-grey-bg; border: 0; - border-top: 3px solid $scipost-lightblue; + border-top: 2px solid $scipost-lightblue; box-shadow: 0 1px 0 0 $card-shadow-color; .card-header { diff --git a/scipost/static/scipost/assets/css/_general.scss b/scipost/static/scipost/assets/css/_general.scss index 0b926b714fcdba25e55fbc2fd887b16e6cfc7e41..cf3454a950c33533763320ef101feecf37dae138 100644 --- a/scipost/static/scipost/assets/css/_general.scss +++ b/scipost/static/scipost/assets/css/_general.scss @@ -33,18 +33,62 @@ body #MathJax_Message { margin: 0 auto; } -footer .logos a { - width: 25%; - display: inline-block; - max-width: 100px; - padding: 0 0.25rem; -} +footer { + background-color: $scipost-darkblue; + color: $white; -// body > .container { -// padding-bottom: 1.5rem; -// } + a, + a:hover { + color: $white; + } + + .logos a { + width: 25%; + display: inline-block; + max-width: 100px; + padding: 0 0.25rem; + } +} .quote-border { border-left: 3px solid $scipost-lightblue; padding-left: 1rem; } + + + +header { + background-color: $scipost-darkblue; + color: $white; + border: 0; + margin: 0; + padding: 0 5px; + + img { + padding: 0; + margin: 10px 20px; + } + + p { + margin: 25px; + } + + .content { + display: flex; + flex-wrap: nowrap; + + .logobox { + align-self: center; + + a { + display: block; + } + } + + .blurbbox { + align-self: center; + margin-left: auto; + text-align: right; + } + } +} diff --git a/scipost/static/scipost/assets/css/_grid.scss b/scipost/static/scipost/assets/css/_grid.scss index d601f5e6428c863f2c20c7618c5e118f66880ea1..b74fc9fe4c8fafb000db178d7e398e3460bded47 100644 --- a/scipost/static/scipost/assets/css/_grid.scss +++ b/scipost/static/scipost/assets/css/_grid.scss @@ -15,6 +15,18 @@ img { display: none; } +footer { + .social-media a { + padding-left: 0.25rem; + padding-right: 0.25rem; + margin-right: 0.25rem; + + &:hover { + color: $scipost-lightblue; + } + } +} + footer.secondary { color: $scipost-darkblue; background: $white; @@ -28,12 +40,20 @@ footer.secondary { .container-inner { - padding: $grid-gutter-width; + padding: ($grid-gutter-width / 2); background-color: $white; } .container { max-width: 1500px; - padding-left: 30px; - padding-right: 30px; +} + +@media screen and (min-width: 768px) { + .container-inner { + padding: $grid-gutter-width; + } + .container { + padding-left: 30px; + padding-right: 30px; + } } diff --git a/scipost/static/scipost/assets/css/_homepage.scss b/scipost/static/scipost/assets/css/_homepage.scss index 52cd38d0f81fd4376799cca61a20f16df4d699dd..4c78a17197bfe52a02ca026e2387eea2c48d3f65 100644 --- a/scipost/static/scipost/assets/css/_homepage.scss +++ b/scipost/static/scipost/assets/css/_homepage.scss @@ -29,6 +29,34 @@ margin-bottom: 1rem; } } + + &.sidebar-left { + .main-panel { + order: 2; + } + .sidebar { + order: 1; + } + } +} + +.sidebar { + margin-bottom: 2rem; + + a { + &.active { + font-weight: 700; + } + } +} + +.container.header { + padding-top: 0.5rem; + margin-bottom: 1.5rem; + + .highlight { + background-color: $white; + } } .granting-institutions > a { @@ -65,6 +93,17 @@ width: 350px; padding-right: 0; } + + &.sidebar-left { + .main-panel { + padding-right: 0; + padding-left: 1rem; + } + .sidebar { + padding-right: 1rem; + padding-left: 0; + } + } } } diff --git a/scipost/static/scipost/assets/css/_journals.scss b/scipost/static/scipost/assets/css/_journals.scss index 490351d34a4027661c60611b65fa9f5d33d07893..f50b7493f412c0e334ce8bcaebf356e5e3411b57 100644 --- a/scipost/static/scipost/assets/css/_journals.scss +++ b/scipost/static/scipost/assets/css/_journals.scss @@ -67,6 +67,7 @@ ul.publicationClickables { display: inline-block; margin: 0 10px; padding: 5px; + width: 100%; &.publicationPDF { font-size: 130%; @@ -74,6 +75,11 @@ ul.publicationClickables { } } } +@media screen and (min-width: 768px) { + ul.publicationClickables li { + width: auto; + } +} .publication { diff --git a/scipost/static/scipost/assets/css/_list_group.scss b/scipost/static/scipost/assets/css/_list_group.scss index b598898a08caa9faf14f2f59088a2c2f1045e000..5b7cf40e37cfd9b0350c3b5f1d912fc3b891238b 100644 --- a/scipost/static/scipost/assets/css/_list_group.scss +++ b/scipost/static/scipost/assets/css/_list_group.scss @@ -45,3 +45,59 @@ ul.references { margin: 0 0 0.2rem 2rem; } } + +ul.links > li.active a { + font-weight: 700; +} + +ul.sortable-list { + li { + background-color: $white; + border: 1px solid $scipost-darkblue; + padding: 0.5rem; + margin-bottom: -1px; + cursor: move; + + &:hover { + background-color: $gray-200; + } + } +} + + +li, +.li { + .thesis, + .commentary, + .submission, + .publication { + .subject { + margin-bottom: 0; + padding-bottom: 0; + color: $text-muted; + } + + .title { + margin-bottom: 0; + } + + .authors { + margin-bottom: 0.5rem; + } + + .meta { + color: $text-muted; + } + + .abstract { + margin-bottom: 0.25rem; + } + } + + .submission, + .commentary { + .meta { + margin-bottom: 0.25rem; + } + } +} diff --git a/scipost/static/scipost/assets/css/_navbar.scss b/scipost/static/scipost/assets/css/_navbar.scss index 8b32a2115a3afdcae2c6d4f521dbdf295a5c026e..0b19649d36ea691cb5273cb4235edf80fd695ba8 100644 --- a/scipost/static/scipost/assets/css/_navbar.scss +++ b/scipost/static/scipost/assets/css/_navbar.scss @@ -3,12 +3,15 @@ * */ .navbar { - - .nav-item.active a, a:hover { text-decoration: underline; } + .nav-item.active a { + font-weight: 700; + text-decoration: underline; + } + .nav-link { color: $white; } @@ -16,9 +19,7 @@ .navbar-nav { flex-direction: row; overflow: auto; - // -ms-overflow-style: none; -webkit-overflow-scrolling: touch; - // overflow: -moz-scrollbars-none; .nav-link { padding-left: .5rem; @@ -28,19 +29,6 @@ } } -// Hide scrollbars... trying to -// ::-webkit-scrollbar, -// ::-webkit-scrollbar-button, -// ::-webkit-scrollbar-track, -// ::-webkit-scrollbar-track-piece, -// ::-webkit-scrollbar-thumb, -// ::-webkit-scrollbar-corner, -// ::-webkit-resizer { -// // display: none; -// background: rgba(0,0,0,0); -// background-color: rgba(0,0,0,0); -// } - .container-outside { &.main-nav { background-color: $scipost-lightblue; @@ -125,9 +113,9 @@ min-width: 16px; line-height: 10px; display: none; - padding: 0.25em; + padding: 0.2em; border-radius: 99px; - border: 1px solid $white; + border: 1px solid $scipost-lightblue; background-color: $white; color: $scipost-lightblue; } diff --git a/scipost/static/scipost/assets/css/_notifications.scss b/scipost/static/scipost/assets/css/_notifications.scss index a607b5af07920ee0261f7ee60c0abd62e64e18ee..3607c064c9515997d5fe15b174317e2ae7136d2f 100644 --- a/scipost/static/scipost/assets/css/_notifications.scss +++ b/scipost/static/scipost/assets/css/_notifications.scss @@ -15,7 +15,7 @@ } } - &.show .fa-inbox { + a[data-toggle="popover"][aria-describedby] { color: $scipost-darkblue; } @@ -37,11 +37,40 @@ .notifications { padding: 0; - min-width: 500px; + min-width: 450px; + border-color: $gray-600; + border-radius: 1px; + .inbox-header, .header { - padding: 1rem 1rem 0.5rem 1rem; - background-color: #f9f9f9; + padding: 0.5rem 0; + border-bottom: 1px solid $gray-600; + margin: 0 1rem; + } + + .inbox-header { + border-top: 1px solid $gray-600; + border-bottom: 0; + margin-top: 0.25rem; + } + + .header { + padding-top: 1rem; + padding-bottom: 1rem; + margin-bottom: 0.25rem; + + h3, + .item { + border: 0; + line-height: 20px; + display: inline-block; + } + + .item { + float: right; + padding-left: 0; + padding-right: 0; + } } li.item { @@ -53,7 +82,7 @@ .item { padding: 0.4rem 1rem; border-radius: 0; - border-top: 1px solid #fff; + border-top: 1px solid $white; border-left: 0; border-right: 0; flex-direction: row; @@ -78,6 +107,30 @@ } } + .live_notify_list { + max-height: 250px; + overflow: scroll; + + &::after { + bottom: 0; + content: ''; + display: block; + width: 100%; + position: absolute; + height: 20px; + box-shadow: inset 0 -3px 9px 0px $gray-600; + } + + .meta { + font-size: 90%; + margin-top: 0.5rem; + } + + .item { + border-color: $gray-600; + } + } + a.item, .item a { color: $scipost-lightblue; @@ -112,7 +165,31 @@ } } + + .links .item { + &.active, + &.active[href]:hover { + background-color: transparent; + } + + &.active { + font-weight: 600; + text-decoration: underline; + } + } + .item:hover .actions { opacity: 1.0; } + + .unavailable { + margin: 0 1rem 0.25rem; + padding: 0.5rem 0 0.75rem; + border-bottom: 1px solid $gray-600; + + .head { + color: $orange; + font-weight: 600; + } + } } diff --git a/scipost/static/scipost/assets/css/_personal_page.scss b/scipost/static/scipost/assets/css/_personal_page.scss index 834f01d4e90045cc17c82b22877f3e7c426f37ed..d2e06dd17613933bf3f8b4e3386a4c8183a0d872 100644 --- a/scipost/static/scipost/assets/css/_personal_page.scss +++ b/scipost/static/scipost/assets/css/_personal_page.scss @@ -1,9 +1,3 @@ table.availabilities { width: 100%; } - -.personalTabMenu { - li { - cursor: pointer; - } -} diff --git a/scipost/static/scipost/assets/css/_pool.scss b/scipost/static/scipost/assets/css/_pool.scss index 8b6dbdff6d5d38938167dbc50a97e8f29ab90914..05a3f70c846ec038910197c1f878c3691a3918e8 100644 --- a/scipost/static/scipost/assets/css/_pool.scss +++ b/scipost/static/scipost/assets/css/_pool.scss @@ -29,7 +29,7 @@ } .loading-container { - .submission_title, + .li.submission, .author_list { display: none; } diff --git a/scipost/static/scipost/assets/css/_popover.scss b/scipost/static/scipost/assets/css/_popover.scss new file mode 100644 index 0000000000000000000000000000000000000000..73aaf85d42437c77f638cbdb4d9c1534105af55a --- /dev/null +++ b/scipost/static/scipost/assets/css/_popover.scss @@ -0,0 +1,124 @@ +.bs-popover-top { + margin-bottom: $popover-arrow-height; + + .arrow { + bottom: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1); + } + + .arrow::before, + .arrow::after { + border-width: $popover-arrow-height ($popover-arrow-width / 2) 0; + } + + .arrow::before { + bottom: 0; + border-top-color: $popover-arrow-outer-color; + } + + .arrow::after { + bottom: $popover-border-width; + border-top-color: $popover-arrow-color; + } +} + +.bs-popover-right { + margin-left: $popover-arrow-height; + + .arrow { + left: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1); + width: $popover-arrow-height; + height: $popover-arrow-width; + margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners + } + + .arrow::before, + .arrow::after { + border-width: ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2) 0; + } + + .arrow::before { + left: 0; + border-right-color: $popover-arrow-outer-color; + } + + .arrow::after { + left: $popover-border-width; + border-right-color: $popover-arrow-color; + } +} + +.bs-popover-bottom { + margin-top: $popover-arrow-height; + + .arrow { + top: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1); + } + + .arrow::before, + .arrow::after { + border-width: 0 ($popover-arrow-width / 2) $popover-arrow-height ($popover-arrow-width / 2); + } + + .arrow::before { + top: 0; + border-bottom-color: $popover-arrow-outer-color; + } + + .arrow::after { + top: $popover-border-width; + border-bottom-color: $popover-arrow-color; + } + + // This will remove the popover-header's border just below the arrow + .popover-header::before { + position: absolute; + top: 0; + left: 50%; + display: block; + width: $popover-arrow-width; + margin-left: ($popover-arrow-width / -2); + content: ""; + border-bottom: $popover-border-width solid $popover-header-bg; + } +} + +.bs-popover-left { + margin-right: $popover-arrow-height; + + .arrow { + right: calc((#{$popover-arrow-height} + #{$popover-border-width}) * -1); + width: $popover-arrow-height; + height: $popover-arrow-width; + margin: $border-radius-lg 0; // make sure the arrow does not touch the popover's rounded corners + } + + .arrow::before, + .arrow::after { + border-width: ($popover-arrow-width / 2) 0 ($popover-arrow-width / 2) $popover-arrow-height; + } + + .arrow::before { + right: 0; + border-left-color: $popover-arrow-outer-color; + } + + .arrow::after { + right: $popover-border-width; + border-left-color: $popover-arrow-color; + } +} + +.bs-popover-auto { + &[x-placement^="top"] { + @extend .bs-popover-top; + } + &[x-placement^="right"] { + @extend .bs-popover-right; + } + &[x-placement^="bottom"] { + @extend .bs-popover-bottom; + } + &[x-placement^="left"] { + @extend .bs-popover-left; + } +} diff --git a/scipost/static/scipost/assets/css/_reports.scss b/scipost/static/scipost/assets/css/_reports.scss index 871bb5accdaf88fc863fe537b54214286203a04a..1398493dca9d69b5a74f207ed724ad5b11303a58 100644 --- a/scipost/static/scipost/assets/css/_reports.scss +++ b/scipost/static/scipost/assets/css/_reports.scss @@ -29,10 +29,12 @@ > ul { list-style: none; display: flex; + flex-wrap: wrap; padding: 0; margin: 0; li { + flex: 100%; border: 1px solid #ddd; margin: 2px 2px; padding: 0.25rem 0.5rem; @@ -40,3 +42,37 @@ } } } + +@media screen and (min-width: 768px) { + .ratings { + > ul { + li { + flex: auto; + } + } + } +} + +.anonymous-alert { + margin-bottom: 0.5rem; + + .anonymous-yes, + .anonymous-no { + font-weight: 600; + } + + .anonymous-yes { + color: $red; + } + .anonymous-no { + color: $green; + } +} + +.report-preview { + .latex-preview { + border: 1px solid $scipost-darkblue; + padding: 0.5rem 0.75rem; + white-space: pre-wrap; + } +} diff --git a/scipost/static/scipost/assets/css/_submissions.scss b/scipost/static/scipost/assets/css/_submissions.scss index f41deda48852a0f3e890647e5527955234e42e60..9f53652a62bb23a828b1700ccfeaeb0eade00acb 100644 --- a/scipost/static/scipost/assets/css/_submissions.scss +++ b/scipost/static/scipost/assets/css/_submissions.scss @@ -47,3 +47,12 @@ table.submission_header { background-color: $green; } } + +.submission { + &.status { + .prefix, + .status { + display: inline-block; + } + } +} diff --git a/scipost/static/scipost/assets/css/_type.scss b/scipost/static/scipost/assets/css/_typography.scss similarity index 92% rename from scipost/static/scipost/assets/css/_type.scss rename to scipost/static/scipost/assets/css/_typography.scss index f6eef699ad9eada0e0f37bebe7f9b58259e326c0..50789f21660d69bc8064ce5a09c7eaff4ff89ec7 100644 --- a/scipost/static/scipost/assets/css/_type.scss +++ b/scipost/static/scipost/assets/css/_typography.scss @@ -13,32 +13,34 @@ a:hover { text-decoration: underline; } h1, h2, h3, h4, h5, h6 { - margin: 0; - padding: 5px 0; + padding-top: 5px; + padding-bottom: 5px; text-shadow: none; } -h1 { - line-height: 1.4; -} - h1 > a { color: $scipost-darkblue; } h3, -h4 { - line-height: normal; -} - -h5 { - font-weight: 300; +.h3, +h4, +.h4, +h5, +.h5, +h6 +.h6 { + margin-bottom: 0; } .text-black { color: $scipost-darkblue; } +.text-underline { + text-decoration: underline; +} + .orange-underline { line-height: 1.3; border-bottom: 2px solid $scipost-orange; @@ -51,7 +53,6 @@ h5 { .container-outside.header { background-color: $gray-200; - // box-shadow: 0 1px 0 0 $card-shadow-color; h1, h2 { @@ -102,7 +103,7 @@ h3.highlight-empty { h5, .h5 { - color: #636c72; + font-weight: 300; } hr, diff --git a/scipost/static/scipost/assets/css/scipost-physics.scss b/scipost/static/scipost/assets/css/scipost-physics.scss index 381822f4dfd9e0796319f1ec57093e9eb95c18a5..858e3465db889e415399115d6c2e34dead7e6f97 100644 --- a/scipost/static/scipost/assets/css/scipost-physics.scss +++ b/scipost/static/scipost/assets/css/scipost-physics.scss @@ -11,18 +11,21 @@ .links { background-color: #ddd; - display: inline-block; + display: flex; font-size: 16px; border-radius: $card-border-radius; border: 1px solid #c5c5c5; margin: 0; width: 100%; padding: 0; + flex-direction: row; + overflow: auto; + list-style: none; > li { - display: inline-block; margin: 0; padding: 0.5rem; + white-space: nowrap; a { padding: 0.25rem 0.75rem; diff --git a/scipost/static/scipost/assets/css/style.scss b/scipost/static/scipost/assets/css/style.scss index 6e6fca92a968ba2d2d382a858ba4c5e410042741..20477d6909fb0efb2e305fa57f8abfae4e100525 100644 --- a/scipost/static/scipost/assets/css/style.scss +++ b/scipost/static/scipost/assets/css/style.scss @@ -34,9 +34,10 @@ @import "notifications"; @import "page_header"; @import "pool"; +@import "popover"; @import "tables"; @import "tooltip"; -@import "type"; +@import "typography"; /** * SciPost Specific diff --git a/scipost/static/scipost/assets/js/notifications.js b/scipost/static/scipost/assets/js/notifications.js index 12f6b849bbcea9efbd0ee7b546e9ea0995cc00f3..6d995a57e6942e803a9d3a4e9b4a9ef2a3511d32 100644 --- a/scipost/static/scipost/assets/js/notifications.js +++ b/scipost/static/scipost/assets/js/notifications.js @@ -63,11 +63,11 @@ function update_list_callback(data, args) { } } if(typeof item.timesince !== 'undefined'){ - message += "<br><small>"; + message += "<div class='meta'>"; if(typeof item.forward_link !== 'undefined') { message += " <a href='" + item.forward_link + "'>Direct link</a> · "; } - message += "<span class='text-muted'>" + item.timesince + " ago</span></small>"; + message += "<span class='text-muted'>" + item.timesince + " ago</span></div>"; } // Notification actions @@ -128,12 +128,23 @@ function initiate_popover() { var template = $('.notifications_container .popover-template').html(); $('.notifications_container a[data-toggle="popover"]').popover({ trigger: 'focus', + animation: false, + offset: '0, 10px', template: template, + delay: { + 'show': 0, + 'hide': 200, + }, placement: 'bottom', + boundary: 'viewport', title: 'empty-on-purpose' }) .on('inserted.bs.popover', function() { $('body').trigger('notification_open_list'); + var self = this; + $('.popover').on('click', function() { + $('.notifications_container a[data-toggle="popover"]').focus(); + }); }) .on('hide.bs.popover', function() { // Bug: force removal of tooltip diff --git a/scipost/static/scipost/assets/js/scripts.js b/scipost/static/scipost/assets/js/scripts.js index 4e96974ec3df7596e5d8cdc9f2fbafa8daedad99..8adbae645179b5aea816d04c6e9f86a7f06e52b5 100644 --- a/scipost/static/scipost/assets/js/scripts.js +++ b/scipost/static/scipost/assets/js/scripts.js @@ -1,7 +1,10 @@ +require('jquery-ui/ui/widgets/sortable'); +require('jquery-ui/ui/disable-selection'); + import notifications from './notifications.js'; function hide_all_alerts() { - $(".alert").fadeOut(300); + $(".alert").remove('.no-dismiss').fadeOut(300); } var activate_tooltip = function() { @@ -12,6 +15,19 @@ var activate_tooltip = function() { }); } + +var sort_form_list = function(list_el) { + $(list_el).sortable({ + update: function(event, ui) { + $.each($(list_el + ' li'), function(index, el) { + $(el).find('input[name$=ORDER]').val(index + 1); + }); + } + }); +}; + + + var getUrlParameter = function getUrlParameter(sParam) { var sPageURL = decodeURIComponent(window.location.search.substring(1)), sURLVariables = sPageURL.split('&'), @@ -53,6 +69,7 @@ function init_page() { }); activate_tooltip(); + sort_form_list('form ul.sortable-list'); } $(function(){ diff --git a/scipost/templates/feeds/latest_publications_title.html b/scipost/templates/feeds/latest_publications_title.html index 6558e2da81b6a1766d69447f71d5d776a4f81485..388e7a3dfc6642a5bbf9408c14da16be46d8e0f6 100644 --- a/scipost/templates/feeds/latest_publications_title.html +++ b/scipost/templates/feeds/latest_publications_title.html @@ -1 +1 @@ -{{obj.in_issue.in_volume.in_journal.get_abbreviation_citation}} {{obj.in_issue.in_volume.number}}, {{obj.get_paper_nr}} ({{obj.publication_date|date:'Y'}}), by {{ obj.author_list }} +{{obj.in_issue.in_volume.in_journal.abbreviation_citation}} {{obj.in_issue.in_volume.number}}, {{obj.get_paper_nr}} ({{obj.publication_date|date:'Y'}}), by {{ obj.author_list }} diff --git a/scipost/templates/partials/scipost/personal_page/account.html b/scipost/templates/partials/scipost/personal_page/account.html index bcb731d9b2bb8a84ff3b75b0403485f9c52db829..bfda86979026a266428a272d69dc8e4e5b2b9d1e 100644 --- a/scipost/templates/partials/scipost/personal_page/account.html +++ b/scipost/templates/partials/scipost/personal_page/account.html @@ -151,7 +151,7 @@ <form action="{% url 'scipost:mark_unavailable_period' %}" method="post"> {% csrf_token %} {{ unavailability_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> </div> <div class="col-md-4 ml-md-5"> diff --git a/scipost/templates/scipost/Fellow_activity_overview.html b/scipost/templates/scipost/Fellow_activity_overview.html index 1d6c2fdf01cc2900cdf5543e367c079bd6c862c8..9530cbfc2858ef4acc5f841def21c990cada21eb 100644 --- a/scipost/templates/scipost/Fellow_activity_overview.html +++ b/scipost/templates/scipost/Fellow_activity_overview.html @@ -19,8 +19,8 @@ </div> </div> -<table class="assignments_listing w-100"> - <thead class=""> +<table class="table table-bordered table-hover"> + <thead class="thead-light"> <tr> <th>Name</th> <th>Expertises</th> diff --git a/scipost/templates/scipost/about.html b/scipost/templates/scipost/about.html index 81a6d25c9a8d329ee16a6025a7b8ef591d0af2c3..240f766f7476fcdf8a2006b71fd915ac8ecb9dc2 100644 --- a/scipost/templates/scipost/about.html +++ b/scipost/templates/scipost/about.html @@ -5,20 +5,12 @@ {% load staticfiles %} {% load scipost_extras %} - -{% block breadcrumb %} - <div class="container-outside header"> - <div class="container"> - <h1>About SciPost</h1> - </div> - </div> -{% endblock %} - {% block content %} <div class="row"> <div class="col"> + <h1 class="highlight">About SciPost</h1> <p>SciPost is a complete scientific publication portal.</p> <p>It is purely online-based, and offers freely, openly, globally and perpetually accessible science.</p> <p>Being managed by professional scientists, and making use of editor-solicited and contributed reviews, its Journals aim at the highest achievable standards of refereeing.</p> diff --git a/scipost/templates/scipost/base_for_sidebar.html b/scipost/templates/scipost/base_for_sidebar.html index 9569677b98e1c6b420dc2e92165e85252b7d626b..7d1e9d6c69adf2cd9b8a81b8c26e1d4c0916cb89 100644 --- a/scipost/templates/scipost/base_for_sidebar.html +++ b/scipost/templates/scipost/base_for_sidebar.html @@ -1,5 +1,7 @@ {% extends 'scipost/bare_base.html' %} +{% block body_class %}{{block.super}} has-sidebar{% endblock %} + {% block base %} <div class="container"> <div class="content-wrapper"> diff --git a/scipost/templates/scipost/change_password.html b/scipost/templates/scipost/change_password.html index dd5f72df1f9d3e4d544cba001cfb9f2f1c74a3be..f8a49984937b21833ac9889ad2bf6fc09cddc05f 100644 --- a/scipost/templates/scipost/change_password.html +++ b/scipost/templates/scipost/change_password.html @@ -18,7 +18,7 @@ <form action="{% url 'scipost:change_password' %}" method="post"> {% csrf_token %} {{form|bootstrap}} - <input type="submit" class="btn btn-secondary" value="Change" /> + <input type="submit" class="btn btn-outline-secondary" value="Change" /> </form> </div> </div> diff --git a/scipost/templates/scipost/claim_authorships.html b/scipost/templates/scipost/claim_authorships.html index 7a5144847a5e021d1f5abf766eada6fa3525dba7..1f9a0f9819222c5b91cec264572e4e158ce5c7ba 100644 --- a/scipost/templates/scipost/claim_authorships.html +++ b/scipost/templates/scipost/claim_authorships.html @@ -34,7 +34,7 @@ <div class="card-footer"> <form class="d-inline-block" action="{% url 'scipost:claim_pub_authorship' publication_id=pub.id claim=1 %}" method="post"> {% csrf_token %} - <input class="btn btn-secondary" type="submit" value="I am an author" /> + <input class="btn btn-outline-secondary" type="submit" value="I am an author" /> </form> <form class="d-inline-block ml-1" action="{% url 'scipost:claim_pub_authorship' publication_id=pub.id claim=0 %}" method="post"> {% csrf_token %} @@ -62,7 +62,7 @@ <div class="card-footer"> <form class="d-inline-block" action="{% url 'scipost:claim_sub_authorship' submission_id=sub.id claim=1 %}" method="post"> {% csrf_token %} - <input class="btn btn-secondary" type="submit" value="I am an author" /> + <input class="btn btn-outline-secondary" type="submit" value="I am an author" /> </form> <form class="d-inline-block ml-1" action="{% url 'scipost:claim_sub_authorship' submission_id=sub.id claim=0 %}" method="post"> {% csrf_token %} @@ -91,7 +91,7 @@ <div class="card-footer"> <form class="d-inline-block" action="{% url 'scipost:claim_com_authorship' commentary_id=com.id claim=1 %}" method="post"> {% csrf_token %} - <input class="btn btn-secondary" type="submit" value="I am an author" /> + <input class="btn btn-outline-secondary" type="submit" value="I am an author" /> </form> <form class="d-inline-block ml-1" action="{% url 'scipost:claim_com_authorship' commentary_id=com.id claim=0 %}" method="post"> {% csrf_token %} @@ -119,7 +119,7 @@ <div class="card-footer"> <form class="d-inline-block" action="{% url 'scipost:claim_thesis_authorship' thesis_id=thesis.id claim=1%}" method="post"> {% csrf_token %} - <input class="btn btn-secondary" type="submit" value="I am an author" /> + <input class="btn btn-outline-secondary" type="submit" value="I am an author" /> </form> <form class="d-inline-block ml-1" action="{% url 'scipost:claim_thesis_authorship' thesis_id=thesis.id claim=0%}" method="post"> {% csrf_token %} diff --git a/scipost/templates/scipost/email_group_members.html b/scipost/templates/scipost/email_group_members.html index c91fd98618ea113df9d5f966aa5373724191adcd..e96e5735f8c5520f4c85d2523398cf49477b69ae 100644 --- a/scipost/templates/scipost/email_group_members.html +++ b/scipost/templates/scipost/email_group_members.html @@ -18,7 +18,7 @@ <form action="{% url 'scipost:email_group_members' %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Send email"/> + <input type="submit" class="btn btn-outline-secondary" value="Send email"/> </form> </div> diff --git a/scipost/templates/scipost/footer.html b/scipost/templates/scipost/footer.html index f792aebbd718746f0af4c4081b2c4e0cb3a68c1b..28c00d337967d8383275d31e48bea2162fb31757 100644 --- a/scipost/templates/scipost/footer.html +++ b/scipost/templates/scipost/footer.html @@ -1,7 +1,7 @@ {% load staticfiles %} <footer class="footer"> - <div class="container-fluid py-1"> - <div class="row my-3"> + <div class="container py-4"> + <div class="row mb-0"> <div class="col-md-4 mb-3 mb-md-0"> Copyright © <a href="{% url 'scipost:foundation' %}" target="_">SciPost Foundation</a> @@ -9,20 +9,42 @@ Contact the <a href="mailto:admin@scipost.org">administrators</a> or <a href="mailto:techsupport@scipost.org">tech support</a> <br/> <a href="{% url 'scipost:terms_and_conditions' %}">Terms and conditions</a> - </div> - <div class="col-md-3 mb-3 mb-md-0"> - Follow us:<br/> - <table> + + <table class="mt-2 social-media"> <tr> - <td><a href="//www.facebook.com/scipost" target="_blank" title="Facebook"><img src="{% static 'scipost/images/FB-f-Logo__white_29.png' %}" width="20" alt="Facebook"/></a></td> - <td><a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"><img src="{% static 'scipost/images/Twitter_Logo_Blue.png' %}" width="32" alt="Twitter"/></a></td> - <td><a style="float: right;" href="{% url 'scipost:feeds' %}"><img src="{% static 'scipost/images/feed-icon-28x28.png' %}" alt="Feed logo" width="20"></a></td> + <td> + <a href="//www.facebook.com/scipost" target="_blank" title="Facebook"> + <i class="fa fa-facebook" aria-hidden="true"></i> + </a> + </td> + <td> + <a href="//twitter.com/scipost_dot_org" target="_blank" title="Twitter"> + <i class="fa fa-twitter" aria-hidden="true"></i> + </a> + </td> + <td> + <a href="{% url 'scipost:feeds' %}" title="RSS feeds"> + <i class="fa fa-rss" aria-hidden="true"></i> + </a> + </td> </tr> </table> </div> - <div class="col-md-5"> + <div class="col-md-4 mb-3 mb-md-0"> <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank"><img alt="Creative Commons License" style="border-width:0" src="//i.creativecommons.org/l/by/4.0/80x15.png" /></a><br />Except where otherwise noted, all content on SciPost is licensed under a <a rel="license" href="//creativecommons.org/licenses/by/4.0/" target="_blank">Creative Commons Attribution 4.0 International License</a>. </div> + <div class="col-md-4 mb-3 mb-md-0 text-right"> + <a href="{% url 'journals:journals' %}">Journals</a> + <br> + <a href="{% url 'submissions:submissions' %}">Submissions</a> + <br> + <a href="{% url 'commentaries:commentaries' %}">Commentaries</a> + <br> + <a href="{% url 'theses:theses' %}">Theses</a> + <br> + <a href="{% url 'scipost:about' %}">About SciPost</a> + <br> + </div> </div> </div> </footer> diff --git a/scipost/templates/scipost/header.html b/scipost/templates/scipost/header.html index 05e6b25b83d15f009b6bb6ac425a03dd8b6a7e8b..be3219ff206a2092147378ca46f7972475790976 100644 --- a/scipost/templates/scipost/header.html +++ b/scipost/templates/scipost/header.html @@ -1,16 +1,16 @@ {% load staticfiles %} <header> <div class="container"> - <div class="flex-container" id="headerflex"> - <div class="flex-logobox"> + <div class="content"> + <div class="logobox"> <a href="{% url 'scipost:index' %}"><img src="{% static 'scipost/images/logo_scipost_RGB_HTML_groot.png' %}" alt="SciPost logo" width="240" /></a> </div> - <div class="flex-blurbbox d-none d-md-block"> - <p> - <i>The complete scientific publication portal</i><br /> - <i>Managed by professional scientists</i><br /> - <i>For open, global and perpetual access to science</i> - </p> + <div class="blurbbox d-none d-md-block"> + <p> + <i>The complete scientific publication portal</i><br /> + <i>Managed by professional scientists</i><br /> + <i>For open, global and perpetual access to science</i> + </p> </div> </div> </div> diff --git a/scipost/templates/scipost/howto_production.html b/scipost/templates/scipost/howto_production.html index 231b31f75bea6422bcdbe292ea929e41af2e4a26..c9c394128bc95a5f31013703d28d35ea41e7869f 100644 --- a/scipost/templates/scipost/howto_production.html +++ b/scipost/templates/scipost/howto_production.html @@ -16,7 +16,7 @@ <p>Version: 2016-12-16</p> - <hr class="hr12"/> + <hr> <ol> <li> diff --git a/scipost/templates/scipost/index.html b/scipost/templates/scipost/index.html index fb119f746bb9f6ed95963de0b7f1d11d31360e92..5546719426f347d435abd1155666200f886a4429 100644 --- a/scipost/templates/scipost/index.html +++ b/scipost/templates/scipost/index.html @@ -3,7 +3,7 @@ {% load render_bundle from webpack_loader %} {% load staticfiles %} -{% block body_class %}{{block.super}} has-sidebar has-breadcrumb-submenu homepage{% endblock %} +{% block body_class %}{{block.super}} has-breadcrumb-submenu homepage{% endblock %} {% block breadcrumb %} <div class="container-outside sub-nav"> @@ -36,7 +36,7 @@ <!-- Latest publications --> <div class="card card-grey px-3"> <div class="card-header px-0"> - <h2><a href="{% url 'journals:journals' %}" class="text-black">Latest Publications</a></h2> + <h2 class="mb-0"><a href="{% url 'journals:journals' %}" class="text-black">Latest Publications</a></h2> </div> <div class="card-body p-0"> <ul class="list-group list-group-flush"> @@ -61,7 +61,7 @@ <!-- Latest submissions --> <div class="card card-grey px-3"> <div class="card-header px-0"> - <h2><a href="{% url 'submissions:submissions' %}" class="text-black">Latest Submissions</a></h2> + <h2 class="mb-0"><a href="{% url 'submissions:submissions' %}" class="text-black">Latest Submissions</a></h2> </div> <div class="card-body p-0"> <ul class="list-group list-group-flush"> diff --git a/scipost/templates/scipost/layout_2_col.html b/scipost/templates/scipost/layout_2_col.html new file mode 100644 index 0000000000000000000000000000000000000000..fa5547e21b19e21db34474eaa011101c422811b7 --- /dev/null +++ b/scipost/templates/scipost/layout_2_col.html @@ -0,0 +1,47 @@ +{% extends 'scipost/bare_base.html' %} + +{% block body_class %}{{block.super}} has-sidebar layout-2-col{% endblock %} + +{% block breadcrumb %} + <div class="container-outside header"> + <div class="container"> + <nav class="breadcrumb hidden-sm-down"> + {% block breadcrumb_items %} + <a href="{% url 'scipost:index' %}" class="breadcrumb-item">SciPost</a> + {% endblock %} + </nav> + </div> + </div> +{% endblock %} + + +{% block base %} + <div class="container mb-4 header"> + {% block page_header %}{% endblock page_header %} + </div> + + <div class="container"> + <div class="content-wrapper"> + <div class="main-panel"> + <div class="container-inner"> + <div class="{% block container_class %}{% endblock %}"> + {% block content %}{% endblock content %} + + {% block content_footer %}{% endblock content_footer %} + </div> + + {% block secondary_footer %}{% endblock secondary_footer %} + </div> + + </div> + + <div class="sidebar {% block sidebar_class %}{% endblock %}"> + <div class="container-inner"> + {% block sidebar %}{% endblock %} + </div> + </div> + </div> + </div> + {% include 'scipost/footer.html' %} + +{% endblock base %} diff --git a/scipost/templates/scipost/navbar.html b/scipost/templates/scipost/navbar.html index 960f025f7c4c84f59f4254da00694af3e3888133..43fcc97810c0547f2097623df562ae982837bccf 100644 --- a/scipost/templates/scipost/navbar.html +++ b/scipost/templates/scipost/navbar.html @@ -10,8 +10,8 @@ <li class="nav-item{% if request.path == '/' %} active{% endif %}"> <a href="{% url 'scipost:index' %}" class="nav-link">Home</a> </li> - <li class="nav-item{% if '/journals/' in request.path %} active{% endif %}"> - <a href="{% url 'journals:journals' %}" class="nav-link">Journals</a> + <li class="nav-item{% if '/journals/publications' in request.path %} active{% endif %}"> + <a href="{% url 'journals:publications' %}" class="nav-link">Publications</a> </li> <li class="nav-item{% if '/submissions/' in request.path %} active{% endif %}"> <a class="nav-link" href="{% url 'submissions:submissions' %}">Submissions</a> @@ -70,10 +70,6 @@ </li> {% endif %} - <li class="nav-item search-item"> - <a class="nav-link" href="{% url 'scipost:search' %}">Search</a> - </li> - </ul> <form action="{% url 'scipost:search' %}" method="get" class="form-inline search-nav-form"> <input class="form-control mr-sm-2" id="id_q" maxlength="100" name="q" type="text" aria-label="Search" value="{{ search_query|default:'' }}"> diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index d76a139812f815609e76571cf0e32d251cc28a70..b48c766d67fdbe009375dcc820a291ad01bb64b3 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -19,7 +19,7 @@ <div class="row"> <div class="col-12"> - <hr class="hr12"> + <hr> <h3>Your credentials will soon be verified by an Editor.</h3> <p>If accepted, you will become a registered Contributor, enabling you to submit, comment and vote.</p> </div> @@ -44,39 +44,39 @@ <div class="tab-nav-inner"> <!-- Nav tabs --> <ul class="nav btn-group personal-page-nav" role="tablist"> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'account' %}sp-autoload='true'{% endif %} href="#account" sp-dynamic-load="{% url 'scipost:personal_page_account' %}" data-toggle="tab">Account</a> </li> {% if is_scipost_admin or is_edcol_admin or is_editorial_college or is_advisory_board or is_vetting_editor or is_ambassador or is_junior_ambassador %} - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'editorial_actions' %}sp-autoload='true'{% endif %} href="#editorial-actions" sp-dynamic-load="{% url 'scipost:personal_page_editorial_actions' %}" data-toggle="tab">Editorial Actions</a> </li> {% endif %} {% if perms.scipost.can_referee %} - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'refereeing' %}sp-autoload='true'{% endif %} data-toggle="tab" href="#refereeing" sp-dynamic-load="{% url 'scipost:personal_page_refereeing' %}">Refereeing {% if refereeing_tab_total_count %}({{refereeing_tab_total_count}}){% endif %}</a> </li> {% endif %} {% if contributor %} {# If user is contributor #} - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'publications' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_publications' %}" href="#publications">Publications</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'submissions' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_submissions' %}" href="#submissions">Submissions</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'commentaries' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_commentaries' %}" href="#commentaries">Commentaries</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> <a class="nav-link" {% if tab == 'theses' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_theses' %}" href="#theses">Theses</a> </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> {% with contributor.comments.regular_comments.awaiting_vetting.count as count %} <a class="nav-link" {% if tab == 'comments' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_comments' %}" href="#comments">Comments{% if count %} ({{count}} unvetted){% endif %}</a> {% endwith %} </li> - <li class="nav-item btn btn-secondary"> + <li class="nav-item btn btn-outline-secondary"> {% with contributor.comments.author_replies.awaiting_vetting.count as count %} <a class="nav-link" {% if tab == 'author_replies' %}sp-autoload='true'{% endif %} data-toggle="tab" sp-dynamic-load="{% url 'scipost:personal_page_author_replies' %}" href="#author-replies">Author Replies{% if count %} ({{count}} unvetted){% endif %}</a> {% endwith %} diff --git a/scipost/templates/scipost/reset_password_confirm.html b/scipost/templates/scipost/reset_password_confirm.html index fe4d5ba196d01f9564082bc223b181a06d1dd59d..3ec7dd517c06f04c15e6a16267daa7a58db65a61 100644 --- a/scipost/templates/scipost/reset_password_confirm.html +++ b/scipost/templates/scipost/reset_password_confirm.html @@ -12,7 +12,7 @@ <form method="post"> {% csrf_token %} {{ form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"> + <input class="btn btn-outline-secondary" type="submit" value="Submit"> </form> {% else %} <p>This reset link is no longer valid!</p> diff --git a/scipost/templates/scipost/send_precooked_email.html b/scipost/templates/scipost/send_precooked_email.html index 9c2b19a536454128bb88a7b0aaf64eed19b1dc24..fe6f5fed4d5f1b5b8154cf8f989b29142c97d9f2 100644 --- a/scipost/templates/scipost/send_precooked_email.html +++ b/scipost/templates/scipost/send_precooked_email.html @@ -18,7 +18,7 @@ <form action="{% url 'scipost:send_precooked_email' %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Send email"/> + <input type="submit" class="btn btn-outline-secondary" value="Send email"/> </form> </div> </div> diff --git a/scipost/templates/scipost/unavailability_period_form.html b/scipost/templates/scipost/unavailability_period_form.html index c014e4cea8bc1fa587dabe090ba4afd366c4283b..832811016eae5ec47afb91189a634503952776e8 100644 --- a/scipost/templates/scipost/unavailability_period_form.html +++ b/scipost/templates/scipost/unavailability_period_form.html @@ -18,7 +18,7 @@ <form method="post"> {% csrf_token %} {{form|bootstrap}} - <input type="submit" class="btn btn-secondary" value="Submit" /> + <input type="submit" class="btn btn-outline-secondary" value="Submit" /> </form> </div> </div> diff --git a/scipost/templates/scipost/vet_authorship_claims.html b/scipost/templates/scipost/vet_authorship_claims.html index fb805bc4c09ce55ad6a0f68dfb544939d922117f..886faf4f6d9f49661cdfaead887402ef6dffc6a2 100644 --- a/scipost/templates/scipost/vet_authorship_claims.html +++ b/scipost/templates/scipost/vet_authorship_claims.html @@ -58,7 +58,7 @@ <div class="card-footer"> <form class="d-inline-block" action="{% url 'scipost:vet_authorship_claim' claim_id=claim.id claim=1%}" method="post"> {% csrf_token %} - <input class="btn btn-secondary px-3" type="submit" value="Accept" /> + <input class="btn btn-outline-secondary px-3" type="submit" value="Accept" /> </form> <form class="d-inline-block ml-1" action="{% url 'scipost:vet_authorship_claim' claim_id=claim.id claim=0%}" method="post"> {% csrf_token %} diff --git a/scipost/templates/scipost/vet_registration_requests.html b/scipost/templates/scipost/vet_registration_requests.html index 45675acc112a77555438c0232881bbb6bf0c228c..c854c00524a4de2b089c517798dab61a815f3ed2 100644 --- a/scipost/templates/scipost/vet_registration_requests.html +++ b/scipost/templates/scipost/vet_registration_requests.html @@ -47,7 +47,7 @@ $(function() { <form action="{% url 'scipost:vet_registration_request_ack' contributor_id=contributor_to_vet.id %}" method="post"> {% csrf_token %} {{form|bootstrap}} - <input type="submit" class="btn btn-secondary" /> + <input type="submit" class="btn btn-outline-secondary" /> </form> </div> </div> diff --git a/scipost/templatetags/filename.py b/scipost/templatetags/filename.py index 6a20f1384cb29b5befd3d6dbf9c8c4a95335c3a1..2bff32b21130e34881c7fbccc41b9084dda4aa3f 100644 --- a/scipost/templatetags/filename.py +++ b/scipost/templatetags/filename.py @@ -8,4 +8,7 @@ register = template.Library() @register.filter def filename(value): - return os.path.basename(value.file.name) + try: + return os.path.basename(value.file.name) + except OSError: + return 'Error: File not found' diff --git a/scipost/templatetags/request_filters.py b/scipost/templatetags/request_filters.py index 124ebd9aeae4a9c3852d32ab5bccea0271b8bad5..e5602d93f8426f28e16589e2ebdd3c5660624718 100644 --- a/scipost/templatetags/request_filters.py +++ b/scipost/templatetags/request_filters.py @@ -25,3 +25,9 @@ def active(context, pattern_or_urlname): if re.search(pattern, path): return 'active' return '' + + +@register.simple_tag(takes_context=True) +def active_get_request(context, get_key, get_value): + query = context['request'].GET.dict() + return 'active' if query.get(get_key) == str(get_value) else '' diff --git a/scipost/test_views.py b/scipost/test_views.py index 22291f792c19630af0cc6b5192667fb3b4c4d8b4..3c6a15c1d384bc65bd1588bd90fd4ec78030f8e7 100644 --- a/scipost/test_views.py +++ b/scipost/test_views.py @@ -2,8 +2,8 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import Group from django.test import TestCase, Client, tag -from commentaries.factories import UnvettedCommentaryFactory, VettedCommentaryFactory,\ - UnpublishedVettedCommentaryFactory +from commentaries.factories import UnvettedCommentaryFactory, CommentaryFactory,\ + UnpublishedCommentaryFactory from commentaries.forms import CommentarySearchForm from commentaries.models import Commentary @@ -81,7 +81,7 @@ class VetCommentaryRequestsTest(TestCase): self.assertEquals(response.context['commentary_to_vet'], None) # Only vetted Commentaries exist! - VettedCommentaryFactory() + CommentaryFactory() response = self.client.get(self.view_url) self.assertEquals(response.context['commentary_to_vet'], None) @@ -96,7 +96,7 @@ class BrowseCommentariesTest(TestCase): fixtures = ['groups', 'permissions'] def setUp(self): - VettedCommentaryFactory(discipline='physics') + CommentaryFactory(discipline='physics') self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', 'nrweeksback': '1' @@ -118,7 +118,7 @@ class CommentaryDetailTest(TestCase): def setUp(self): self.client = Client() - self.commentary = UnpublishedVettedCommentaryFactory() + self.commentary = UnpublishedCommentaryFactory() self.target = reverse( 'commentaries:commentary', kwargs={'arxiv_or_DOI_string': self.commentary.arxiv_or_DOI_string} diff --git a/scipost/urls.py b/scipost/urls.py index b1bb8f2df417d0a4e2f7e98a6ab4400dac360f13..6cfe265c321b2ae7ab0e63aba3345cbd5f817d77 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -7,7 +7,7 @@ from .feeds import LatestNewsFeedRSS, LatestNewsFeedAtom, LatestCommentsFeedRSS, LatestPublicationsFeedRSS, LatestPublicationsFeedAtom from journals import views as journals_views -from journals.constants import REGEX_CHOICES +from journals.constants import REGEX_CHOICES, PUBLICATION_DOI_REGEX from submissions import views as submission_views JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES @@ -185,16 +185,16 @@ urlpatterns = [ name='author_reply_detail'), # Publication detail (+pdf) - url(r'^10.21468/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})$', + url(r'^10.21468/(?P<doi_label>{regex})$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.publication_detail, name='publication_detail'), - url(r'^(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})$', + url(r'^(?P<doi_label>{regex})$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.publication_detail, name='publication_detail'), - url(r'^10.21468/(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/pdf$', + url(r'^10.21468/(?P<doi_label>{regex})/pdf$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.publication_detail_pdf, name='publication_pdf'), - url(r'^(?P<doi_label>[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,})/pdf$', + url(r'^(?P<doi_label>{regex})/pdf$'.format(regex=PUBLICATION_DOI_REGEX), journals_views.publication_detail_pdf, name='publication_pdf'), diff --git a/scipost/views.py b/scipost/views.py index 3097897d4be736bc225a97543b6cc026cebd921c..961dcea9e3c0526b62805907bbde192550ff0348 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -71,10 +71,6 @@ class SearchView(SearchView): ctx = super().get_context_data(*args, **kwargs) ctx['search_query'] = self.request.GET.get('q') ctx['results_count'] = kwargs['object_list'].count() - - # Methods not supported by Whoosh engine - # ctx['stats_results'] = kwargs['object_list'].stats_results() - # ctx['facet_counts'] = kwargs['object_list'].facet('text').facet_counts() return ctx @@ -511,7 +507,7 @@ def _personal_page_publications(request): contributor = request.user.contributor context = { 'contributor': contributor, - 'own_publications': contributor.publications.order_by('-publication_date') + 'own_publications': contributor.publications.published().order_by('-publication_date') } context['nr_publication_authorships_to_claim'] = Publication.objects.filter( author_list__contains=request.user.last_name).exclude( @@ -712,9 +708,6 @@ def _update_personal_data_contributor(request): cont_form.propagate_orcid() messages.success(request, 'Your personal data has been updated.') return redirect(reverse('scipost:update_personal_data')) - else: - user_form = UpdateUserDataForm(instance=contributor.user) - cont_form = UpdatePersonalDataForm(instance=contributor) context = { 'user_form': user_form, diff --git a/submissions/factories.py b/submissions/factories.py index d9c32c745fd079caed4ab17d02fd3511c0605ff9..6231ec36adcb4cd8b0ea9cd86c4155ffbc482898 100644 --- a/submissions/factories.py +++ b/submissions/factories.py @@ -1,108 +1,175 @@ import factory import pytz +import random -from django.utils import timezone - +from comments.factories import SubmissionCommentFactory from scipost.constants import SCIPOST_SUBJECT_AREAS from scipost.models import Contributor from journals.constants import SCIPOST_JOURNALS_DOMAINS -from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal +from common.helpers import random_arxiv_identifier_without_version_number, random_scipost_journal,\ + random_scipost_report_doi_label from .constants import STATUS_UNASSIGNED, STATUS_EIC_ASSIGNED, STATUS_RESUBMISSION_INCOMING,\ STATUS_PUBLISHED, SUBMISSION_TYPE, STATUS_RESUBMITTED, STATUS_VETTED,\ REFEREE_QUALIFICATION, RANKING_CHOICES, QUALITY_SPEC, REPORT_REC,\ REPORT_STATUSES, STATUS_UNVETTED, STATUS_DRAFT -from .models import Submission, Report, RefereeInvitation +from .models import Submission, Report, RefereeInvitation, EICRecommendation, EditorialAssignment from faker import Faker class SubmissionFactory(factory.django.DjangoModelFactory): - class Meta: - model = Submission - author_list = factory.Faker('name') submitted_by = factory.Iterator(Contributor.objects.all()) + submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0]) submitted_to_journal = factory.Sequence(lambda n: random_scipost_journal()) - title = factory.lazy_attribute(lambda x: Faker().sentence()) - abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) + title = factory.Faker('sentence') + abstract = factory.Faker('paragraph', nb_sentences=10) arxiv_link = factory.Faker('uri') arxiv_identifier_wo_vn_nr = factory.Sequence( - lambda n: random_arxiv_identifier_without_version_number()) + lambda n: random_arxiv_identifier_without_version_number()) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) - abstract = Faker().paragraph() - author_comments = Faker().paragraph() - remarks_for_editors = Faker().paragraph() - submission_type = factory.Iterator(SUBMISSION_TYPE, getter=lambda c: c[0]) + abstract = factory.Faker('paragraph') + author_comments = factory.Faker('paragraph') + remarks_for_editors = factory.Faker('paragraph') is_current = True + arxiv_vn_nr = 1 + arxiv_link = factory.lazy_attribute(lambda o: ( + 'https://arxiv.org/abs/%s' % o.arxiv_identifier_wo_vn_nr)) + arxiv_identifier_w_vn_nr = factory.lazy_attribute(lambda o: '%sv%i' % ( + o.arxiv_identifier_wo_vn_nr, o.arxiv_vn_nr)) + submission_date = factory.Faker('date_this_decade') + latest_activity = factory.LazyAttribute(lambda o: Faker().date_time_between( + start_date=o.submission_date, end_date="now", tzinfo=pytz.UTC)) - @factory.post_generation - def fill_arxiv_fields(self, create, extracted, **kwargs): - '''Fill empty arxiv fields.''' - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier_wo_vn_nr - self.arxiv_identifier_w_vn_nr = '%sv1' % self.arxiv_identifier_wo_vn_nr - self.arxiv_vn_nr = kwargs.get('arxiv_vn_nr', 1) + class Meta: + model = Submission @factory.post_generation def contributors(self, create, extracted, **kwargs): - contributors = list(Contributor.objects.order_by('?')[:4]) + contributors = Contributor.objects.all() + if self.editor_in_charge: + contributors = contributors.exclude(id=self.editor_in_charge.id) + contributors = contributors.order_by('?')[:random.randint(1, 6)] # Auto-add the submitter as an author - self.submitted_by = contributors.pop() + self.submitted_by = contributors[0] + self.author_list = ', '.join([ + '%s %s' % (c.user.first_name, c.user.last_name) for c in contributors]) if not create: return - self.authors.add(self.submitted_by) # Add three random authors - for contrib in contributors: - self.authors.add(contrib) - self.author_list += ', %s %s' % (contrib.user.first_name, contrib.user.last_name) - - @factory.post_generation - def dates(self, create, extracted, **kwargs): - timezone.now() - if kwargs.get('submission', False): - self.submission_date = kwargs['submission'] - self.cycle.update_deadline() - return - self.submission_date = Faker().date_time_between(start_date="-3y", end_date="now", - tzinfo=pytz.UTC).date() - self.latest_activity = Faker().date_time_between(start_date=self.submission_date, - end_date="now", tzinfo=pytz.UTC) + self.authors.add(*contributors) self.cycle.update_deadline() class UnassignedSubmissionFactory(SubmissionFactory): - '''This Submission is a 'new request' by a Contributor for its Submission.''' + """ + A new incoming Submission without any EIC assigned. + """ status = STATUS_UNASSIGNED class EICassignedSubmissionFactory(SubmissionFactory): + """ + A Submission with an EIC assigned, visible in the pool and refereeing in process. + """ status = STATUS_EIC_ASSIGNED open_for_commenting = True open_for_reporting = True + @factory.lazy_attribute + def editor_in_charge(self): + return Contributor.objects.order_by('?').first() + @factory.post_generation - def eic(self, create, extracted, **kwargs): - '''Assign an EIC to submission.''' - author_ids = list(self.authors.values_list('id', flat=True)) - self.editor_in_charge = (Contributor.objects.order_by('?') - .exclude(pk=self.submitted_by.pk) - .exclude(pk__in=author_ids).first()) + def eic_assignment(self, create, extracted, **kwargs): + if create: + EditorialAssignmentFactory(submission=self, to=self.editor_in_charge) + + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + for i in range(random.randint(1, 3)): + RefereeInvitationFactory(submission=self) + + for i in range(random.randint(0, 2)): + AcceptedRefereeInvitationFactory(submission=self) + for i in range(random.randint(0, 2)): + FulfilledRefereeInvitationFactory(submission=self) -class ResubmittedSubmissionFactory(SubmissionFactory): - '''This Submission is a `resubmitted` version.''' + @factory.post_generation + def comments(self, create, extracted, **kwargs): + if create: + for i in range(random.randint(0, 3)): + SubmissionCommentFactory(content_object=self) + + @factory.post_generation + def eic_recommendation(self, create, extracted, **kwargs): + if create: + EICRecommendationFactory(submission=self) + + +class ResubmittedSubmissionFactory(EICassignedSubmissionFactory): + """ + A Submission that has a newer Submission version in the database + with a successive version number. + """ status = STATUS_RESUBMITTED open_for_commenting = False open_for_reporting = False is_current = False is_resubmission = False + @factory.post_generation + def successive_submission(self, create, extracted, **kwargs): + """ + Generate a second Submission that's the successive version of the resubmitted Submission + """ + if create and extracted is not False: + # Prevent infinite loops by checking the extracted argument + ResubmissionFactory(arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr, + previous_submission=False) -class ResubmissionFactory(SubmissionFactory): + @factory.post_generation + def gather_successor_data(self, create, extracted, **kwargs): + """ + Gather some data from Submission with same arxiv id such that this Submission + more or less looks like any regular real resubmission. + """ + submission = Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude( + arxiv_vn_nr=self.arxiv_vn_nr).first() + if not submission: + return + + self.author_list = submission.author_list + self.submitted_by = submission.submitted_by + self.editor_in_charge = submission.editor_in_charge + self.submission_type = submission.submission_type + self.submitted_to_journal = submission.submitted_to_journal + self.title = submission.title + self.subject_area = submission.subject_area + self.domain = submission.domain + self.title = submission.title + self.authors.set(self.authors.all()) + + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + """ + This Submission is deactivated for refereeing. + """ + for i in range(random.randint(0, 2)): + FulfilledRefereeInvitationFactory(submission=self) + + for i in range(random.randint(1, 3)): + CancelledRefereeInvitationFactory(submission=self) + + +class ResubmissionFactory(EICassignedSubmissionFactory): """ This Submission is a newer version of a Submission which is already known by the SciPost database. @@ -111,63 +178,99 @@ class ResubmissionFactory(SubmissionFactory): open_for_commenting = True open_for_reporting = True is_resubmission = True + arxiv_vn_nr = 2 @factory.post_generation - def fill_arxiv_fields(self, create, extracted, **kwargs): - '''Fill empty arxiv fields.''' - self.arxiv_link = 'https://arxiv.org/abs/%s' % self.arxiv_identifier_wo_vn_nr - self.arxiv_identifier_w_vn_nr = '%sv2' % self.arxiv_identifier_wo_vn_nr - self.arxiv_vn_nr = 2 + def previous_submission(self, create, extracted, **kwargs): + if create and extracted is not False: + # Prevent infinite loops by checking the extracted argument + ResubmittedSubmissionFactory(arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr, + successive_submission=False) @factory.post_generation - def eic(self, create, extracted, **kwargs): - '''Assign an EIC to submission.''' - author_ids = list(self.authors.values_list('id', flat=True)) - self.editor_in_charge = (Contributor.objects.order_by('?') - .exclude(pk=self.submitted_by.pk) - .exclude(pk__in=author_ids).first()) + def gather_predecessor_data(self, create, extracted, **kwargs): + """ + Gather some data from Submission with same arxiv id such that this Submission + more or less looks like any regular real resubmission. + """ + submission = Submission.objects.filter( + arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).exclude( + arxiv_vn_nr=self.arxiv_vn_nr).first() + if not submission: + return + + self.author_list = submission.author_list + self.submitted_by = submission.submitted_by + self.editor_in_charge = submission.editor_in_charge + self.submission_type = submission.submission_type + self.submitted_to_journal = submission.submitted_to_journal + self.title = submission.title + self.subject_area = submission.subject_area + self.domain = submission.domain + self.title = submission.title + self.authors.set(self.authors.all()) @factory.post_generation - def dates(self, create, extracted, **kwargs): - """Overwrite the parent `dates` method to skip the update_deadline call.""" - timezone.now() - if kwargs.get('submission', False): - self.submission_date = kwargs['submission'] - return - self.submission_date = Faker().date_time_between(start_date="-3y", end_date="now", - tzinfo=pytz.UTC).date() - self.latest_activity = Faker().date_time_between(start_date=self.submission_date, - end_date="now", tzinfo=pytz.UTC) + def referee_invites(self, create, extracted, **kwargs): + """ + Referees for resubmissions are invited once the cycle has been chosen. + """ + pass -class PublishedSubmissionFactory(SubmissionFactory): +class PublishedSubmissionFactory(EICassignedSubmissionFactory): status = STATUS_PUBLISHED open_for_commenting = False open_for_reporting = False + @factory.post_generation + def generate_publication(self, create, extracted, **kwargs): + if create and extracted is not False: + from journals.factories import PublicationFactory + PublicationFactory( + journal=self.submitted_to_journal, + accepted_submission=self, title=self.title, author_list=self.author_list) -class ReportFactory(factory.django.DjangoModelFactory): - class Meta: - model = Report + @factory.post_generation + def eic_assignment(self, create, extracted, **kwargs): + if create: + EditorialAssignmentFactory(submission=self, to=self.editor_in_charge, completed=True) + @factory.post_generation + def referee_invites(self, create, extracted, **kwargs): + for i in range(random.randint(2, 4)): + FulfilledRefereeInvitationFactory(submission=self) + + for i in range(random.randint(0, 2)): + CancelledRefereeInvitationFactory(submission=self) + + +class ReportFactory(factory.django.DjangoModelFactory): status = factory.Iterator(REPORT_STATUSES, getter=lambda c: c[0]) submission = factory.Iterator(Submission.objects.all()) - date_submitted = Faker().date_time_between(start_date="-3y", end_date="now", tzinfo=pytz.UTC) + date_submitted = factory.Faker('date_time_this_decade') vetted_by = factory.Iterator(Contributor.objects.all()) author = factory.Iterator(Contributor.objects.all()) - qualification = factory.Iterator(REFEREE_QUALIFICATION, getter=lambda c: c[0]) - strengths = Faker().paragraph() - weaknesses = Faker().paragraph() - report = Faker().paragraph() - requested_changes = Faker().paragraph() - validity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - significance = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - originality = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - clarity = factory.Iterator(RANKING_CHOICES, getter=lambda c: c[0]) - formatting = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) - grammar = factory.Iterator(QUALITY_SPEC, getter=lambda c: c[0]) - recommendation = factory.Iterator(REPORT_REC, getter=lambda c: c[0]) - remarks_for_editors = Faker().paragraph() + strengths = factory.Faker('paragraph') + weaknesses = factory.Faker('paragraph') + report = factory.Faker('paragraph') + requested_changes = factory.Faker('paragraph') + + qualification = factory.Iterator(REFEREE_QUALIFICATION[1:], getter=lambda c: c[0]) + validity = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + significance = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + originality = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + clarity = factory.Iterator(RANKING_CHOICES[1:], getter=lambda c: c[0]) + formatting = factory.Iterator(QUALITY_SPEC[1:], getter=lambda c: c[0]) + grammar = factory.Iterator(QUALITY_SPEC[1:], getter=lambda c: c[0]) + recommendation = factory.Iterator(REPORT_REC[1:], getter=lambda c: c[0]) + + remarks_for_editors = factory.Faker('paragraph') + flagged = factory.Faker('boolean', chance_of_getting_true=10) + anonymous = factory.Faker('boolean', chance_of_getting_true=75) + + class Meta: + model = Report class DraftReportFactory(ReportFactory): @@ -182,26 +285,88 @@ class UnVettedReportFactory(ReportFactory): class VettedReportFactory(ReportFactory): status = STATUS_VETTED + needs_doi = True + doideposit_needs_updating = factory.Faker('boolean') + doi_label = factory.lazy_attribute(lambda n: random_scipost_report_doi_label()) + pdf_report = factory.Faker('file_name', extension='pdf') class RefereeInvitationFactory(factory.django.DjangoModelFactory): + submission = factory.SubFactory('submissions.factories.SubmissionFactory') + referee = factory.lazy_attribute(lambda o: Contributor.objects.exclude( + id__in=o.submission.authors.all()).order_by('?').first()) + + title = factory.lazy_attribute(lambda o: o.referee.title) + first_name = factory.lazy_attribute(lambda o: o.referee.user.first_name) + last_name = factory.lazy_attribute(lambda o: o.referee.user.last_name) + email_address = factory.lazy_attribute(lambda o: o.referee.user.email) + date_invited = factory.lazy_attribute(lambda o: o.submission.latest_activity) + nr_reminders = factory.lazy_attribute(lambda o: random.randint(0, 4)) + date_last_reminded = factory.lazy_attribute(lambda o: o.submission.latest_activity) + + invitation_key = factory.Faker('md5') + invited_by = factory.lazy_attribute(lambda o: o.submission.editor_in_charge) + class Meta: model = RefereeInvitation - submission = factory.SubFactory('submissions.factories.SubmissionFactory') - referee = factory.Iterator(Contributor.objects.all()) - invitation_key = factory.Faker('md5') - invited_by = factory.Iterator(Contributor.objects.all()) +class AcceptedRefereeInvitationFactory(RefereeInvitationFactory): + accepted = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) @factory.post_generation - def contributor_fields(self, create, extracted, **kwargs): - self.title = self.referee.title - self.first_name = self.referee.user.first_name - self.last_name = self.referee.user.last_name - self.email_address = self.referee.user.email + def report(self, create, extracted, **kwargs): + if create: + VettedReportFactory(submission=self.submission, author=self.referee) -class AcceptedRefereeInvitationFactory(RefereeInvitationFactory): +class FulfilledRefereeInvitationFactory(AcceptedRefereeInvitationFactory): + fulfilled = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) + + @factory.post_generation + def report(self, create, extracted, **kwargs): + if create: + VettedReportFactory(submission=self.submission, author=self.referee) + + +class CancelledRefereeInvitationFactory(AcceptedRefereeInvitationFactory): + fulfilled = False + cancelled = True + date_responded = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.date_invited, end_date="now", tzinfo=pytz.UTC)) + + +class EICRecommendationFactory(factory.django.DjangoModelFactory): + submission = factory.Iterator(Submission.objects.all()) + date_submitted = factory.lazy_attribute(lambda o: Faker().date_time_between( + start_date=o.submission.submission_date, end_date="now", tzinfo=pytz.UTC)) + remarks_for_authors = factory.Faker('paragraph') + requested_changes = factory.Faker('paragraph') + remarks_for_editorial_college = factory.Faker('paragraph') + recommendation = factory.Iterator(REPORT_REC[1:], getter=lambda c: c[0]) + version = 1 + active = True + + class Meta: + model = EICRecommendation + + +class EditorialAssignmentFactory(factory.django.DjangoModelFactory): + """ + A EditorialAssignmentFactory should always have a `submission` explicitly assigned. This will + mostly be done using the post_generation hook in any SubmissionFactory. + """ + submission = None + to = factory.Iterator(Contributor.objects.all()) accepted = True - date_responded = Faker().date_time_between(start_date="-1y", end_date="now", tzinfo=pytz.UTC) + deprecated = False + completed = False + date_created = factory.lazy_attribute(lambda o: o.submission.latest_activity) + date_answered = factory.lazy_attribute(lambda o: o.submission.latest_activity) + + class Meta: + model = EditorialAssignment diff --git a/submissions/forms.py b/submissions/forms.py index 7dc9bc305a3a998a3c345678da02edf64172fe09..850fcbc52a7d743d9eb4747012ab219cf081e4f5 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -495,11 +495,8 @@ class VotingEligibilityForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Do we need this discipline filter still with the new Pool construction??? - # -- JdW; Oct 20th, 2017 self.fields['eligible_fellows'].queryset = Contributor.objects.filter( fellowships__pool=self.instance.submission, - discipline=self.instance.submission.discipline, expertises__contains=[self.instance.submission.subject_area] ).order_by('user__last_name') diff --git a/submissions/management/commands/create_submissions.py b/submissions/management/commands/create_submissions.py new file mode 100644 index 0000000000000000000000000000000000000000..575f01ec60414c1293e1c5dd342d606be7a07b4c --- /dev/null +++ b/submissions/management/commands/create_submissions.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand + +from submissions import factories + + +class Command(BaseCommand): + help = 'Create random Submission objects by using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of submissions to add', + ) + parser.add_argument( + '-s', '--status', + choices=['unassigned', 'assigned', 'resubmitted', 'resubmission', 'published'], + action='store', dest='status', default='assigned', + help='Current status of the Submission', + ) + + def handle(self, *args, **kwargs): + if kwargs['number']: + self.create_submissions(kwargs['number'], status=kwargs['status']) + + def create_submissions(self, n, status='assigned'): + if status == 'unassigned': + factories.UnassignedSubmissionFactory.create_batch(n) + elif status == 'assigned': + factories.EICassignedSubmissionFactory.create_batch(n) + elif status == 'resubmitted': + factories.ResubmittedSubmissionFactory.create_batch(n) + elif status == 'resubmission': + factories.ResubmissionFactory.create_batch(n) + elif status == 'published': + factories.PublishedSubmissionFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Submissions.'.format(n=n))) diff --git a/submissions/migrations/0010_auto_20180314_1607.py b/submissions/migrations/0010_auto_20180314_1607.py new file mode 100644 index 0000000000000000000000000000000000000000..7bbd633195f6d1b3e4dfb44a9552f18d9586275f --- /dev/null +++ b/submissions/migrations/0010_auto_20180314_1607.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-03-14 15:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0009_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='submission_type', + field=models.CharField(choices=[('Letter', 'Letter (broad-interest breakthrough results)'), ('Article', 'Article (in-depth reports on specialized research)'), ('Review', 'Review (candid snapshot of current research in a given area)')], default='', max_length=10), + preserve_default=False, + ), + ] diff --git a/submissions/models.py b/submissions/models.py index 61d1f8555517f1afd33a60a1a6dd7b0a5c1f589b..95c4c2df27f360282c3f8cc176d3a4ab1268ed64 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -66,8 +66,7 @@ class Submission(models.Model): related_name='pool') subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, verbose_name='Primary subject area', default='Phys:QP') - submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE, - blank=True, null=True, default=None) + submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE) submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, related_name='submitted_submissions') voting_fellows = models.ManyToManyField('colleges.Fellowship', blank=True, @@ -139,11 +138,9 @@ class Submission(models.Model): header += ' (current version)' else: header += ' (deprecated version ' + str(self.arxiv_vn_nr) + ')' - try: + if hasattr(self, 'publication') and self.publication.is_published: header += ' (published as %s (%s))' % ( self.publication.doi_string, self.publication.publication_date.strftime('%Y')) - except Publication.DoesNotExist: - pass return header def touch(self): diff --git a/submissions/signals.py b/submissions/signals.py index be833cb833d351eab88e1d0d384e638ef4992762..9bb75251f1999445672a0cbfcaff2a5a5242c16b 100644 --- a/submissions/signals.py +++ b/submissions/signals.py @@ -85,3 +85,17 @@ def notify_invitation_overdue(sender, instance, created, **kwargs): verb=(' would like to remind you that your Refereeing Task is overdue, ' 'please submit your Report'), target=instance.submission, type=NOTIFICATION_REFEREE_OVERDUE) + + +def notify_manuscript_accepted(sender, instance, created, **kwargs): + """ + Notify authors about their manuscript decision. + + instance --- Submission + """ + college = Group.objects.get(name='Editorial College') + authors = User.objects.filter(contributor__submissions=instance) + for user in authors: + notify.send(sender=sender, recipient=user, actor=college, + verb=' has accepted your manuscript for publication.', + target=instance) diff --git a/submissions/templates/partials/submissions/pool/submission_details.html b/submissions/templates/partials/submissions/pool/submission_details.html index d8b17662f5918fb642d1aa70ac453b794fcfd5ad..bc0e006926e75d812324d4f9a9f33afdd495c099 100644 --- a/submissions/templates/partials/submissions/pool/submission_details.html +++ b/submissions/templates/partials/submissions/pool/submission_details.html @@ -94,7 +94,7 @@ {# Accepted submission actions #} {% if submission.status == 'accepted' %} <li><a href="{% url 'submissions:treated_submission_pdf_compile' submission.arxiv_identifier_w_vn_nr %}">Update the Refereeing Package pdf</a></li> - <li>After proofs have been accepted, you can <a href="{% url 'journals:initiate_publication' %}">initiate the publication process</a> (leads to the validation page)</li> + <li><a href="{% url 'journals:update_publication' submission.arxiv_identifier_w_vn_nr %}">Draft Publication</a></li> {% endif %} </ul> diff --git a/submissions/templates/partials/submissions/remark_form.html b/submissions/templates/partials/submissions/remark_form.html index f2872b2a19755ab5d12e5f230171d049ad97a865..5144eea030929d0ba91b890bb7684da2b10c23c5 100644 --- a/submissions/templates/partials/submissions/remark_form.html +++ b/submissions/templates/partials/submissions/remark_form.html @@ -20,15 +20,15 @@ $(document).ready(function(){ <form action="{% url 'submissions:add_remark' submission.arxiv_identifier_w_vn_nr %}" method="post" class="pb-2"> {% csrf_token %} {{ form|bootstrap:'0,12' }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> {% else %} - <button class="btn btn-secondary mb-2" data-toggle="toggle" data-target="#remarkForm{{ submission.id }}" id="remarkButton{{ submission.id }}">Add a remark on this Submission</button> + <button class="btn btn-outline-secondary mb-2" data-toggle="toggle" data-target="#remarkForm{{ submission.id }}" id="remarkButton{{ submission.id }}">Add a remark on this Submission</button> <div class="submitRemarkForm pb-2" id="remarkForm{{ submission.id }}" style="display:none;"> <form action="{% url 'submissions:add_remark' submission.arxiv_identifier_w_vn_nr %}" method="post"> {% csrf_token %} {{ form|bootstrap:'0,12' }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> </div> {% endif %} diff --git a/submissions/templates/partials/submissions/report_preview.html b/submissions/templates/partials/submissions/report_preview.html new file mode 100644 index 0000000000000000000000000000000000000000..e783a89efc1158f052c6f4de207cc359c46f4e9f --- /dev/null +++ b/submissions/templates/partials/submissions/report_preview.html @@ -0,0 +1,44 @@ +<h3 class="mb-4">Preview of your report as it will publicly be shown</h3> + +<div class="report report-preview"> + <div class="reportid"> + <h3><span data-receive="report-identity"><span if-anonymous="true" style="display: none;">Anonymous</span><span if-anonymous="false" style="display: none;">{{ request.user.first_name }} {{ request.user.last_name }}</span> on <span data-receive="report-date">{% now "SHORT_DATE_FORMAT" %}</span></h3> + </div> + + <div class="row"> + <div class="col"> + <h3 class="highlight tight">Strengths</h3> + <div class="pl-md-4"><p data-receive="report-strengths"></p></div> + </div> + </div> + <div class="row"> + <div class="col"> + <h3 class="highlight tight">Weaknesses</h3> + <div class="pl-md-4"><p data-receive="report-weaknesses"></p></div> + </div> + </div> + <div class="row"> + <div class="col"> + <h3 class="highlight tight">Report</h3> + <div class="pl-md-4"><p data-receive="report-report"></p></div> + </div> + </div> + <div class="row"> + <div class="col"> + <h3 class="highlight tight">Requested changes</h3> + <div class="pl-md-4"><p data-receive="report-requested_changes"></p></div> + </div> + </div> + + <div class="ratings"> + <ul> + <li>validity: <span data-receive="report-validity"></span></li> + <li>significance: <span data-receive="report-significance"></span></li> + <li>originality: <span data-receive="report-originality"></span></li> + <li>clarity: <span data-receive="report-clarity"></span></li> + <li>formatting: <span data-receive="report-formatting"></span></li> + <li>grammar: <span data-receive="report-grammar"></span></li> + </ul> + </div> + +</div> diff --git a/submissions/templates/partials/submissions/submission_card_content.html b/submissions/templates/partials/submissions/submission_card_content.html index aa9128c75d3e9ac2f5ff0e87f181f13e5c4bcceb..6d85c8e89df696506f0bb331158aa91b377d6796 100644 --- a/submissions/templates/partials/submissions/submission_card_content.html +++ b/submissions/templates/partials/submissions/submission_card_content.html @@ -1,11 +1,11 @@ -{% include 'partials/submissions/submission_title.html' with submission=submission %} +{% extends 'partials/submissions/submission_li.html' %} {% block card_footer %} - <p class="text-muted mb-0"> + <p class="meta"> Version {{ submission.arxiv_vn_nr }} ({% if submission.is_current %}current version{% else %}deprecated version {{ submission.arxiv_vn_nr }}{% endif %}) <br> - {% if submission.publication %} - Published as <a href="{{ submission.publication.get_absolute_url }}">{{ submission.publication.in_issue.in_volume.in_journal.get_abbreviation_citation }} <strong>{{ submission.publication.in_issue.in_volume.number }}</strong>, {{ submission.publication.get_paper_nr }} ({{ submission.publication.publication_date|date:'Y' }})</a> + {% if submission.publication and submission.publication.is_published %} + Published as <a href="{{ submission.publication.get_absolute_url }}">{{ submission.publication.in_issue.in_volume.in_journal.abbreviation_citation }} <strong>{{ submission.publication.in_issue.in_volume.number }}</strong>, {{ submission.publication.get_paper_nr }} ({{ submission.publication.publication_date|date:'Y' }})</a> {% else %} Submitted {{ submission.submission_date }} to {{ submission.get_submitted_to_journal_display }} {% endif %} · latest activity: {{ submission.latest_activity }} diff --git a/submissions/templates/partials/submissions/submission_card_content_homepage.html b/submissions/templates/partials/submissions/submission_card_content_homepage.html index 8a0ba52f29bbe99148348e51435ef6e926cfd1a8..18dac36ba2e7bd46f4829e9c6e25cf2bac0540c8 100644 --- a/submissions/templates/partials/submissions/submission_card_content_homepage.html +++ b/submissions/templates/partials/submissions/submission_card_content_homepage.html @@ -1,7 +1,7 @@ {% extends 'partials/submissions/submission_card_content.html' %} {% block card_footer %} - <div class="text-muted mb-0"> + <div class="meta"> Submitted {{submission.submission_date}} to {{submission.get_submitted_to_journal_display}}</span> </div> {% endblock %} diff --git a/submissions/templates/partials/submissions/submission_li.html b/submissions/templates/partials/submissions/submission_li.html new file mode 100644 index 0000000000000000000000000000000000000000..c0202ef2b1bb890db63491bf6f117dbe91c77d1f --- /dev/null +++ b/submissions/templates/partials/submissions/submission_li.html @@ -0,0 +1,6 @@ +<div class="li submission"> + <h5 class="subject">{{ submission.get_subject_area_display }}</h5> + <h3 class="title"><a href="{{ submission.get_absolute_url }}">{{ submission.title }}</a></h3> + <p class="authors">by {{ submission.author_list }}</p> + {% block card_footer %}{% endblock %} +</div> diff --git a/submissions/templates/partials/submissions/submission_status.html b/submissions/templates/partials/submissions/submission_status.html index f4812d1d3a9a41215a6f8d8c637707088e4d4e41..2f3e16e7fbf6782fdb84023c6dbfb5c10fccc770 100644 --- a/submissions/templates/partials/submissions/submission_status.html +++ b/submissions/templates/partials/submissions/submission_status.html @@ -1,7 +1,15 @@ -<h4 class="d-inline-block">Current status:</h4> -<div class="d-inline"> - <span class="label label-secondary">{{submission.get_status_display}}</span> - {% if submission.publication %} - as <a href="{{submission.publication.get_absolute_url}}">{{submission.publication.in_issue.in_volume.in_journal.get_abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} ({{submission.publication.publication_date|date:'Y'}})</a> - {% endif %} +<div class="submission status"> + <h6 class="prefix">Current status:</h6> + <div class="status"> + <span class="label label-secondary">{{submission.get_status_display}}</span> + {% if submission.publication and submission.publication.is_published %} + as <a href="{{submission.publication.get_absolute_url}}"> + {% if submission.publication.in_issue %} + {{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} + {% else %} + {{submission.publication.in_journal.abbreviation_citation}}, {{submission.publication.paper_nr}} + {% endif %} + ({{submission.publication.publication_date|date:'Y'}})</a> + {% endif %} + </div> </div> diff --git a/submissions/templates/partials/submissions/submission_title.html b/submissions/templates/partials/submissions/submission_title.html deleted file mode 100644 index 8545ab7f5dbc8a51a5521c6139ad7f24183deb04..0000000000000000000000000000000000000000 --- a/submissions/templates/partials/submissions/submission_title.html +++ /dev/null @@ -1,7 +0,0 @@ -<div class="submission_title"> - <h5 class="pb-0 subject_area">{{ submission.get_subject_area_display }}</h5> - <h3 class="card-title mb-0 submisssion_title"> - <a href="{{ submission.get_absolute_url }}">{{ submission.title }}</a> - </h3> - <p class="mb-3 author_list">by {{ submission.author_list }}</p> -</div> diff --git a/submissions/templates/submissions/admin/editorial_assignment_form.html b/submissions/templates/submissions/admin/editorial_assignment_form.html index 17e933e6bd6a2d1b425ad0d695b0c0baa03e629e..df473a80d47ac289a9f7303c1965dff8c4344d05 100644 --- a/submissions/templates/submissions/admin/editorial_assignment_form.html +++ b/submissions/templates/submissions/admin/editorial_assignment_form.html @@ -57,7 +57,7 @@ <form action="{% url 'submissions:assign_submission' arxiv_identifier_w_vn_nr=submission_to_assign.arxiv_identifier_w_vn_nr %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> </div> </div> diff --git a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html index 48c6e011de6e85b3e053426b71dc861f6dfa49cd..67c157592a55c34617d064ac1bcb1f052b409060 100644 --- a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html +++ b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html @@ -14,7 +14,7 @@ <h1 class="highlight">Prepare Editorial Recommendation for Voting</h1> -{% include 'partials/submissions/submission_title.html' with submission=recommendation.submission %} +{% include 'partials/submissions/submission_li.html' with submission=recommendation.submission %} {% include 'partials/submissions/pool/submission_info_table.html' with submission=recommendation.submission %} <br> diff --git a/submissions/templates/submissions/admin/report_compile_form.html b/submissions/templates/submissions/admin/report_compile_form.html index 5be44d16ee6fec7ee94639e9e26941d100861f61..6bea35822dd1804482bf391b6badef032685723c 100644 --- a/submissions/templates/submissions/admin/report_compile_form.html +++ b/submissions/templates/submissions/admin/report_compile_form.html @@ -35,7 +35,7 @@ <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Upload"/> + <input class="btn btn-outline-secondary" type="submit" value="Upload"/> </form> </div> </div> diff --git a/submissions/templates/submissions/pool/assignments.html b/submissions/templates/submissions/pool/assignments.html index 6b03faec25814696b8779db89d2d61e2b623293e..9d9161994ca9cd7e775944a1c9efbd99b7e98fc2 100644 --- a/submissions/templates/submissions/pool/assignments.html +++ b/submissions/templates/submissions/pool/assignments.html @@ -26,7 +26,7 @@ {% for assignment in current_assignments %} {% if not forloop.first %}<hr class="small">{% endif %} - {% include 'partials/submissions/submission_title.html' with submission=assignment.submission %} + {% include 'partials/submissions/submission_li.html' with submission=assignment.submission %} {% include 'partials/submissions/pool/submission_info_table.html' with submission=assignment.submission %} {% include 'partials/submissions/pool/required_actions_block.html' with submission=submission %} diff --git a/submissions/templates/submissions/pool/editorial_page.html b/submissions/templates/submissions/pool/editorial_page.html index 3f0bb62ec032cdfe8a90c89a93bef812d87411b4..31a0d61ca514f136e00b50c5a4c522ef1e19d2a9 100644 --- a/submissions/templates/submissions/pool/editorial_page.html +++ b/submissions/templates/submissions/pool/editorial_page.html @@ -168,7 +168,7 @@ {{ set_deadline_form|bootstrap_inline:'0,12' }} <div class="ml-2 form-group row"> <div class="col-12"> - <input class="btn btn-secondary" type="submit" value="Set deadline"/> + <input class="btn btn-outline-secondary" type="submit" value="Set deadline"/> </div> </div> </form> diff --git a/submissions/templates/submissions/pool/recommendation.html b/submissions/templates/submissions/pool/recommendation.html index 15af3dc47af8d3aa0995d5580ca5385c2679b688..61aeab9f9c925a003f4a35f4af8e914cf1060f73 100644 --- a/submissions/templates/submissions/pool/recommendation.html +++ b/submissions/templates/submissions/pool/recommendation.html @@ -14,7 +14,7 @@ <h1>Editorial Recommendation to vote on</h1> - {% include 'partials/submissions/submission_title.html' with submission=recommendation.submission %} + {% include 'partials/submissions/submission_li.html' with submission=recommendation.submission %} <a class="d-inline-block mb-3" href="{{ recommendation.submission.get_absolute_url }}" target="_blank">View Reports and Submission details</a> @@ -82,7 +82,7 @@ <form action="{% url 'submissions:vote_on_rec' rec_id=recommendation.id %}" method="post"> {% csrf_token %} {{ form|bootstrap:'0,12' }} - <input type="submit" name="submit" value="Cast your vote" class="btn btn-primary submitButton" id="submit-id-submit"> + <input type="submit" name="submit" value="Cast your vote" class="btn btn-primary" id="submit-id-submit"> </form> {% endif %} diff --git a/submissions/templates/submissions/referee_invitations_decline.html b/submissions/templates/submissions/referee_invitations_decline.html index 8923b77afa84d2f41305e5d473a3b6698b578526..950cf215b3c5abf74aea4fcfc513de14b7a2a80a 100644 --- a/submissions/templates/submissions/referee_invitations_decline.html +++ b/submissions/templates/submissions/referee_invitations_decline.html @@ -34,7 +34,7 @@ $(document).ready(function(){ <form action="{% url 'submissions:decline_ref_invitation' invitation_key=invitation.invitation_key %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Submit" /> + <input type="submit" class="btn btn-outline-secondary" value="Submit" /> </form> </div> </div> diff --git a/submissions/templates/submissions/report_form.html b/submissions/templates/submissions/report_form.html index 5c6c813aefffc2db5a0df7c883b9dfd8b90beb7b..84b69c692cf125c1b5ab90effb2b5580cf4847a2 100644 --- a/submissions/templates/submissions/report_form.html +++ b/submissions/templates/submissions/report_form.html @@ -10,56 +10,46 @@ {% block pagetitle %}: submit report{% endblock pagetitle %} {% block content %} - -<script> - $(document).ready(function(){ - - var strengths_input = $("#id_strengths"); - function set_strengths(value) { - $("#preview-strengths").text(value) - } - set_strengths(strengths_input.val()) - strengths_input.keyup(function(){ - var new_text = $(this).val() - set_strengths(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var weaknesses_input = $("#id_weaknesses"); - function set_weaknesses(value) { - $("#preview-weaknesses").text(value) - } - set_weaknesses(weaknesses_input.val()) - weaknesses_input.keyup(function(){ - var new_text = $(this).val() - set_weaknesses(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var report_input = $("#id_report"); - function set_report(value) { - $("#preview-report").text(value) - } - set_report(report_input.val()) - report_input.keyup(function(){ - var new_text = $(this).val() - set_report(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - var requested_changes_input = $("#id_requested_changes"); - function set_requested_changes(value) { - $("#preview-requested_changes").text(value) - } - set_requested_changes(requested_changes_input.val()) - requested_changes_input.keyup(function(){ - var new_text = $(this).val() - set_requested_changes(new_text) - MathJax.Hub.Queue(["Typeset",MathJax.Hub]); - }) - - }); -</script> + <script> + $(function(){ + function set_preview(el) { + $('[data-receive$="' + $(el).attr('id').split('id_')[1] + '"]').text($(el).val()) + } + function set_preview_select(el) { + $('[data-receive$="' + $(el).attr('id').split('id_')[1] + '"]').text($(el).find('option:selected').text()) + } + function update_identity_preview(show_identity) { + $('[data-receive="report-identity"] [if-anonymous]').hide(); + if (show_identity) { + $('[data-receive="report-identity"] [if-anonymous="false"]').show(); + } else { + $('[data-receive="report-identity"] [if-anonymous="true"]').show(); + } + } + $('#id_weaknesses, #id_strengths, #id_report, #id_requested_changes').on('keyup', function(){ + set_preview(this) + if (typeof MathJax !== "undefined") { + // First trigger will fail since MathJax is loaded in the footer. + MathJax.Hub.Queue(["Typeset",MathJax.Hub]); + } + }).trigger('keyup'); + $('#id_validity, #id_originality, #id_significance, #id_clarity, #id_formatting, #id_grammar').on('change', function(){ + set_preview_select(this); + }).trigger('change'); + + $('input[name$="anonymous"]').on('change', function() { + $('.anonymous-alert').show() + .children('h3').hide() + if ($(this).prop('checked')) { + update_identity_preview(false); + $('.anonymous-yes').show(); + } else { + update_identity_preview(true); + $('.anonymous-no').show(); + } + }).trigger('change'); + }); + </script> {% if user.is_authenticated %} @@ -71,18 +61,18 @@ <div class="row"> <div class="col-12"> - <h2>Submission</h2> + <h3>Submission</h3> {% include 'partials/submissions/submission_summary.html' with submission=submission show_abstract=1 %} </div> </div> - <hr> + <hr class="divider"> <div class="row"> <div class="col-12"> <div class="card card-grey"> <div class="card-body"> - <h1>Your {% if form.instance.is_followup_report %}followup {% endif %}report:</h1> - <p>A preview of text areas will appear below as you type (you can use LaTeX \$...\$ for in-text equations or \ [ ... \ ] for on-line equations).</p> + <h2>Your {% if form.instance.is_followup_report %}followup {% endif %}report:</h2> + <p>A preview of text areas will appear below as you type (you can use $\LaTeX$ \$...\$ for in-text equations or \ [ ... \ ] for on-line equations).</p> <p class="mb-0">Any fields with an asterisk (*) are required.</p> {% if form.instance.is_followup_report %} <p class="mb-0"> @@ -97,41 +87,35 @@ </div> </div> {% endif %} - <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> - {% csrf_token %} - {{ form|bootstrap:'3,9' }} - <p>Any fields with an asterisk (*) are required.</p> - <input class="btn btn-primary" type="submit" name="save_submit" value="Submit your report"/> - <input class="btn btn-secondary ml-2" type="submit" name="save_draft" value="Save your report as draft"/> - {% if form.report_type == 'report_post_edrec' %} - <div class="card border-warning mt-4"> - <div class="card-body">The Editorial Recommendation for this Submission has already been formulated. Therefore, your report will be labelled as <label class="label label-warning">Post-Editorial Recommendation Report</label>. + + <div class="row"> + <div class="col-md-6"> + <br> + <form action="{% url 'submissions:submit_report' arxiv_identifier_w_vn_nr=submission.arxiv_identifier_w_vn_nr %}" method="post"> + {% csrf_token %} + {{ form|bootstrap:'12,12' }} + <div class="anonymous-alert" style="display: none;"> + <h3 class="anonymous-yes">Your Report will remain anonymous.</h3> + <h3 class="anonymous-no"><span class="text-danger">Your Report will be <span class="text-underline">signed</span>.</span> Thank you very much!</h3> </div> - </div> - {% endif %} - <div class="my-4"> - <em>By clicking on Submit, you state that you abide by the <a href="{% url 'journals:journals_terms_and_conditions' %}#referee_code_of_conduct">referee code of conduct</a>.</em> + <p>Any fields with an asterisk (*) are required.</p> + <input class="btn btn-primary" type="submit" name="save_submit" value="Submit your report"/> + <input class="btn btn-outline-secondary ml-2" type="submit" name="save_draft" value="Save your report as draft"/> + {% if form.report_type == 'report_post_edrec' %} + <div class="card border-warning mt-4"> + <div class="card-body">The Editorial Recommendation for this Submission has already been formulated. Therefore, your report will be labelled as <label class="label label-warning">Post-Editorial Recommendation Report</label>. + </div> + </div> + {% endif %} + <div class="my-4"> + <em>By clicking on Submit, you state that you abide by the <a href="{% url 'journals:journals_terms_and_conditions' %}#referee_code_of_conduct" target="_blank">referee code of conduct</a>.</em> + </div> + </form> </div> - </form> - </div> - </div> - - <hr> - <div class="row"> - <div class="col-12"> - <h3>Preview of your report (text areas only):</h3> - - <h4>Strengths:</h4> - <p class="p-2" id="preview-strengths"></p> - - <h4>Weaknesses:</h4> - <p class="p-2" id="preview-weaknesses"></p> - - <h4>Report:</h4> - <p class="p-2" id="preview-report"></p> - - <h4>Requested changes:</h4> - <p class="p-2" id="preview-requested_changes"></p> + <div class="col-md-6"> + {% include 'partials/submissions/report_preview.html' %} + </div> + </div> </div> </div> diff --git a/submissions/templates/submissions/submission_detail.html b/submissions/templates/submissions/submission_detail.html index 0d6dd800738d0cec86087a939f8efd0969f93639..1c27d127c1bfbeb5d0239821f8e7e4a1766f4453 100644 --- a/submissions/templates/submissions/submission_detail.html +++ b/submissions/templates/submissions/submission_detail.html @@ -26,8 +26,8 @@ <h3 class="mb-3">by {{submission.author_list}}</h3> <div class="pl-2"> - {% if submission.publication %} - <h3>- Published as <a href="{{submission.publication.get_absolute_url}}">{{submission.publication.in_issue.in_volume.in_journal.get_abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} ({{submission.publication.publication_date|date:'Y'}})</a></h3> + {% if submission.publication and submission.publication.is_published %} + <h3>- Published as <a href="{{submission.publication.get_absolute_url}}">{{submission.publication.in_issue.in_volume.in_journal.abbreviation_citation}} <strong>{{submission.publication.in_issue.in_volume.number}}</strong>, {{submission.publication.get_paper_nr}} ({{submission.publication.publication_date|date:'Y'}})</a></h3> {% endif %} @@ -42,25 +42,6 @@ {% if not submission.is_current %} <h3><span class="text-danger">- This is not the current version.</span></h3> {% endif %} - - {% comment %} - {% if submission.other_versions or not submission.is_current %} - <ul class="mt-3 mb-1 list-unstyled pl-4"> - {% if not submission.is_current %} - <li><h3 class="text-danger">This is not the current version.</h3></li> - {% endif %} - - {% if submission.other_versions %} - <li>Other versions of this Submission (with Reports) exist:</li> - <ul class="list-unstyled"> - {% for vn in submission.other_versions %} - <li>{% include 'partials/submissions/submission_version.html' with submission=vn %}</li> - {% endfor %} - </ul> - {% endif %} - </ul> - {% endif %} - {% endcomment %} </div> <h3 class="mt-2">Submission summary</h3> @@ -101,7 +82,7 @@ {% endfor %} <div class="mb-4"> - <h2>Events</h2> + <h3>Events</h3> <div id="eventslist"> {% include 'partials/submissions/submission_events.html' with events=submission.events.for_author %} </div> @@ -111,7 +92,7 @@ {% if is_author or user|is_in_group:'Editorial Administrators' %} {% if submission.production_stream.proofs.for_authors.exists %} <div class="mb-4" id="proofsslist"> - <h2>Proofs</h2> + <h3>Proofs</h3> <ul> {% for proofs in submission.production_stream.proofs.for_authors %} <li> diff --git a/submissions/templates/submissions/submission_list.html b/submissions/templates/submissions/submission_list.html index 05e114e3f2e255678c6c9b80c6ce870f309e436f..e0c3d2e8b80a2e3ebf6e20d3dc638d90278c66b2 100644 --- a/submissions/templates/submissions/submission_list.html +++ b/submissions/templates/submissions/submission_list.html @@ -23,7 +23,7 @@ <h2 class="card-title">Search SciPost Submissions:</h2> <form action="{% url 'submissions:submissions' %}" class="small" method="get"> {{ form|bootstrap:'4,8,sm' }} - <input class="btn btn-sm btn-secondary" type="submit" value="Search"/> + <input class="btn btn-sm btn-outline-secondary" type="submit" value="Search"/> </form> </div> </div> @@ -44,7 +44,7 @@ <div class="row"> <div class="col-12"> {% if recent %} - <h2>Recent Submissions:</h2> + <h2>Recent Submissions{% if to_journal %} to {{ to_journal }}{% endif %}:</h2> {% elif browse %} <h2>Submissions in {{ discipline }} in the last {{ nrweeksback }} week{% if nrweeksback == '1' %}{% else %}s{% endif %}:</h2> {% else %} diff --git a/submissions/templates/submissions/submission_prefill_form.html b/submissions/templates/submissions/submission_prefill_form.html index 1a9a35f5320ecc925991b712295943c386055749..d21582d742ad850a684a8c5a921f087dd151bc23 100644 --- a/submissions/templates/submissions/submission_prefill_form.html +++ b/submissions/templates/submissions/submission_prefill_form.html @@ -42,7 +42,7 @@ <form action="{% url 'submissions:prefill_using_identifier' %}" method="post"> {% csrf_token %} {{ form|bootstrap }} - <input type="submit" class="btn btn-secondary" value="Query arXiv"/> + <input type="submit" class="btn btn-outline-secondary" value="Query arXiv"/> </form> </div> </div> diff --git a/submissions/templates/submissions/treated_submission_pdf_compile.html b/submissions/templates/submissions/treated_submission_pdf_compile.html index 8412eed0c6aa982c23f6e9ac7268c078f70a9561..b6038f713782ec69f5c3a1f1bc5256ffb8078e89 100644 --- a/submissions/templates/submissions/treated_submission_pdf_compile.html +++ b/submissions/templates/submissions/treated_submission_pdf_compile.html @@ -43,7 +43,7 @@ <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Upload"/> + <input class="btn btn-outline-secondary" type="submit" value="Upload"/> </form> </div> </div> diff --git a/submissions/utils.py b/submissions/utils.py index 0610215599b89987167ca1540b4a2c06b0db2849..dcf46c20424e7e761a3cbbe88d7a8a9d7f5b680f 100644 --- a/submissions/utils.py +++ b/submissions/utils.py @@ -118,9 +118,6 @@ class BaseSubmissionCycle: Reset the reporting deadline according to current datetime and default cycle length. New reporting deadline may be explicitly given as datetime instance. """ - if self.submission.status == STATUS_RESUBMISSION_INCOMING: - raise CycleUpdateDeadlineError('Submission has invalid status: %s' - % self.submission.status) delta_d = period or self.default_days deadline = timezone.now() + datetime.timedelta(days=delta_d) self.submission.reporting_deadline = deadline diff --git a/submissions/views.py b/submissions/views.py index 382fa621324a8a87fcf27089e76503a435768ce9..da7983022760b0242c0cfd6655d18ab2ed22e821 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -1,5 +1,6 @@ import datetime import feedparser +import strings from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required @@ -33,22 +34,21 @@ from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSe EICRecommendationForm, ReportForm, VetReportForm, VotingEligibilityForm,\ SubmissionCycleChoiceForm, ReportPDFForm, SubmissionReportsForm,\ iThenticateReportForm, SubmissionPoolFilterForm +from .signals import notify_manuscript_accepted from .utils import SubmissionUtils from colleges.permissions import fellowship_required, fellowship_or_admin_required -from mails.views import MailEditingSubView -from scipost.forms import ModifyPersonalMessageForm, RemarkForm -from scipost.mixins import PaginationMixin -from scipost.models import Contributor, Remark - from comments.forms import CommentForm from invitations.constants import INVITATION_REFEREEING from invitations.models import RegistrationInvitation +from journals.models import Journal from mails.utils import DirectMailUtil +from mails.views import MailEditingSubView from production.forms import ProofsDecisionForm from production.models import ProductionStream - -import strings +from scipost.forms import ModifyPersonalMessageForm, RemarkForm +from scipost.mixins import PaginationMixin +from scipost.models import Contributor, Remark ############### @@ -136,10 +136,10 @@ class SubmissionListView(PaginationMixin, ListView): def get_queryset(self): queryset = Submission.objects.public_newest() self.form = self.form(self.request.GET) - if 'to_journal' in self.kwargs: + if 'to_journal' in self.request.GET: queryset = queryset.filter( latest_activity__gte=timezone.now() + datetime.timedelta(days=-60), - submitted_to_journal=self.kwargs['to_journal'] + submitted_to_journal=self.request.GET['to_journal'] ) elif 'discipline' in self.kwargs and 'nrweeksback' in self.kwargs: discipline = self.kwargs['discipline'] @@ -161,8 +161,12 @@ class SubmissionListView(PaginationMixin, ListView): context['form'] = self.form # To customize display in the template - if 'to_journal' in self.kwargs: - context['to_journal'] = self.kwargs['to_journal'] + if 'to_journal' in self.request.GET: + try: + context['to_journal'] = Journal.objects.filter( + name=self.request.GET['to_journal']).first().get_name_display() + except (Journal.DoesNotExist, AttributeError): + context['to_journal'] = self.request.GET['to_journal'] if 'discipline' in self.kwargs: context['discipline'] = self.kwargs['discipline'] context['nrweeksback'] = self.kwargs['nrweeksback'] @@ -1570,6 +1574,7 @@ def fix_College_decision(request, rec_id): # Add SubmissionEvent for authors # Do not write a new event for minor/major modification: already done at moment of # creation. + notify_manuscript_accepted(request.user, submission, False) submission.add_event_for_author('An Editorial Recommendation has been formulated: %s.' % recommendation.get_recommendation_display()) elif recommendation.recommendation == -3: diff --git a/theses/factories.py b/theses/factories.py index c5461a1a6160fe74397f854ed00309966ed6c06c..91e4e2edce654395ed1be66847ba6ad83459f3c3 100644 --- a/theses/factories.py +++ b/theses/factories.py @@ -1,7 +1,5 @@ import factory -from django.utils import timezone - from common.helpers.factories import FormFactory from journals.constants import SCIPOST_JOURNALS_DOMAINS from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS @@ -11,32 +9,45 @@ from .models import ThesisLink from .forms import VetThesisLinkForm from .constants import THESIS_TYPES -from faker import Faker - -timezone.now() - -class ThesisLinkFactory(factory.django.DjangoModelFactory): +class BaseThesisLinkFactory(factory.django.DjangoModelFactory): class Meta: model = ThesisLink + abstract = True requested_by = factory.Iterator(Contributor.objects.all()) + vetted_by = factory.Iterator(Contributor.objects.all()) + vetted = True + type = factory.Iterator(THESIS_TYPES, getter=lambda c: c[0]) domain = factory.Iterator(SCIPOST_JOURNALS_DOMAINS, getter=lambda c: c[0]) discipline = factory.Iterator(SCIPOST_DISCIPLINES, getter=lambda c: c[0]) subject_area = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: c[0]) - title = factory.Faker('text') + title = factory.Faker('sentence') pub_link = factory.Faker('uri') author = factory.Faker('name') supervisor = factory.Faker('name') institution = factory.Faker('company') - defense_date = factory.Faker('date') - abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) - - -class VettedThesisLinkFactory(ThesisLinkFactory): - vetted_by = factory.Iterator(Contributor.objects.all()) - vetted = True + defense_date = factory.Faker('date_this_decade') + abstract = factory.Faker('paragraph') + + @factory.post_generation + def author_as_cont(self, create, extracted, **kwargs): + if not create: + # Simple build, do nothing. + return + + if extracted: + # A list of groups were passed in, use them + for contributor in extracted: + self.author_as_cont.add(contributor) + elif factory.Faker('boolean'): + contributor = Contributor.objects.order_by('?').first() + self.author_as_cont.add(contributor) + + +class ThesisLinkFactory(BaseThesisLinkFactory): + pass class VetThesisLinkFormFactory(FormFactory): diff --git a/theses/management/__init__.py b/theses/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/theses/management/commands/__init__.py b/theses/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/theses/management/commands/create_theses.py b/theses/management/commands/create_theses.py new file mode 100644 index 0000000000000000000000000000000000000000..8c98134929bcf3ff3c5aecc8be334ba2df9c900e --- /dev/null +++ b/theses/management/commands/create_theses.py @@ -0,0 +1,19 @@ +from django.core.management.base import BaseCommand + +from theses import factories + + +class Command(BaseCommand): + help = 'Create random Thesis objects using the factories.' + + def add_arguments(self, parser): + parser.add_argument( + 'number', action='store', default=0, type=int, + help='Number of Theses to add') + + def handle(self, *args, **kwargs): + self.create_theses(kwargs['number']) + + def create_theses(self, n): + factories.ThesisLinkFactory.create_batch(n) + self.stdout.write(self.style.SUCCESS('Successfully created {n} Theses.'.format(n=n))) diff --git a/theses/templates/theses/_thesislink_card_content.html b/theses/templates/theses/_thesislink_card_content.html index fdca0c00f25b0315d7d9d70cc449942a1b2e73c0..f78ff5c16ed8551c0793f7e70fb66127a0aff1a7 100644 --- a/theses/templates/theses/_thesislink_card_content.html +++ b/theses/templates/theses/_thesislink_card_content.html @@ -1,13 +1,15 @@ -<div class="card-body"> - <h5 class="pb-0">{{ thesislink.get_discipline_display }} · {{ thesislink.get_domain_display }} · {{ thesislink.get_subject_area_display }}</h5> - <h3> - <a href="{% url 'theses:thesis' thesislink_id=thesislink.id %}">{{ thesislink.title }}</a> - </h3> - <p class="mt-0 mb-3"> - {{ thesislink.get_type_display }} thesis by {{ thesislink.author }} - (supervisor(s): {{ thesislink.supervisor }}) - </p> - <p class="card-text text-muted"> - Defense date: {{ thesislink.defense_date }} · Latest activity: {{ thesislink.latest_activity|date:"DATE_FORMAT" }} - </p> +<div class="card-body px-0"> + <div class="li thesis"> + <h5 class="subject">{{ thesislink.get_discipline_display }} · {{ thesislink.get_domain_display }} · {{ thesislink.get_subject_area_display }}</h5> + <h3 class="title"> + <a href="{% url 'theses:thesis' thesislink_id=thesislink.id %}">{{ thesislink.title }}</a> + </h3> + <p class="authors"> + {{ thesislink.get_type_display }} thesis by {{ thesislink.author }} + (supervisor(s): {{ thesislink.supervisor }}) + </p> + <p class="meta mb-2"> + Defense date: {{ thesislink.defense_date }} · Latest activity: {{ thesislink.latest_activity|date:"DATE_FORMAT" }} + </p> + </div> </div> diff --git a/theses/templates/theses/thesislink_list.html b/theses/templates/theses/thesislink_list.html index 366df71def6d21942c125873c06967d3493d055d..468529133cb9c6cc35c79a1fa7f6e121a19cfc8f 100644 --- a/theses/templates/theses/thesislink_list.html +++ b/theses/templates/theses/thesislink_list.html @@ -26,7 +26,7 @@ <h2 class="card-title">Search SciPost Theses:</h2> <form class="small" action="{% url 'theses:theses' %}" method="get"> {{ form|bootstrap:'4,8,sm' }} - <input class="btn btn-sm btn-secondary" type="submit" value="Search" /> + <input class="btn btn-outline-secondary" type="submit" value="Search" /> </form> </div> </div> diff --git a/theses/test_views.py b/theses/test_views.py index 91e37fc3c6b157d72e479765f8dc7d3c5ba6344e..33d8eed56faa8bd9fa7cafd75a8ff150f0eed209 100644 --- a/theses/test_views.py +++ b/theses/test_views.py @@ -13,7 +13,7 @@ from comments.factories import CommentFactory from comments.forms import CommentForm from comments.models import Comment from .views import RequestThesisLink, VetThesisLink, thesis_detail -from .factories import ThesisLinkFactory, VettedThesisLinkFactory, VetThesisLinkFormFactory +from .factories import ThesisLinkFactory, ThesisLinkFactory, VetThesisLinkFormFactory from .models import ThesisLink from .forms import VetThesisLinkForm from common.helpers import model_form_data @@ -163,14 +163,14 @@ class TestTheses(TestCase): self.target = reverse('theses:theses') def test_empty_search_query(self): - thesislink = VettedThesisLinkFactory() + thesislink = ThesisLinkFactory() response = self.client.get(self.target) search_results = response.context["object_list"] self.assertTrue(thesislink in search_results) def test_search_query_on_author(self): - thesislink = VettedThesisLinkFactory() - other_thesislink = VettedThesisLinkFactory() + thesislink = ThesisLinkFactory() + other_thesislink = ThesisLinkFactory() form_data = {'author': thesislink.author} response = self.client.get(self.target, form_data) search_results = response.context['object_list'] diff --git a/virtualmeetings/templates/virtualmeetings/VGM_detail.html b/virtualmeetings/templates/virtualmeetings/VGM_detail.html index 230b9b4e8207bd86b227390e961b0bec461fcd4b..c3e7b279978cf42805616d53fb7d193f2bba3079 100644 --- a/virtualmeetings/templates/virtualmeetings/VGM_detail.html +++ b/virtualmeetings/templates/virtualmeetings/VGM_detail.html @@ -53,13 +53,13 @@ <div class="card card-grey my-2"> <div class="card-header"> <h2>Feedback on SciPost</h2> - <a href="javascript:;" class="btn btn-secondary" data-toggle="toggle" data-target="#submitFeedback">Provide feedback</a> + <a href="javascript:;" class="btn btn-outline-secondary" data-toggle="toggle" data-target="#submitFeedback">Provide feedback</a> </div> <div class="card-body" style="display: none;" id="submitFeedback"> <form action="{% url 'virtualmeetings:feedback' VGM_id=VGM.id %}" method="post"> {% csrf_token %} {{ feedback_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"/> + <input class="btn btn-outline-secondary" type="submit" value="Submit"/> </form> </div> </div> @@ -74,12 +74,12 @@ <li id="feedback{{feedback.id}}"> {% include 'virtualmeetings/feedback_content.html' with feedback=feedback %} </li> - <a href="javascript:;" class="btn btn-secondary" data-toggle="toggle" data-target="#remarkfeedbackForm{{ feedback.id }}">Add a remark on this Feedback</a> + <a href="javascript:;" class="btn btn-outline-secondary" data-toggle="toggle" data-target="#remarkfeedbackForm{{ feedback.id }}">Add a remark on this Feedback</a> <div class="py-2" id="remarkfeedbackForm{{ feedback.id }}"> <form action="{% url 'virtualmeetings:add_remark_on_feedback' VGM_id=VGM.id feedback_id=feedback.id %}" method="post"> {% csrf_token %} {{ remark_form|bootstrap:'0,12' }} - <input class="btn btn-secondary" type="submit" value="Submit" /> + <input class="btn btn-outline-secondary" type="submit" value="Submit" /> </form> </div> {% if feedback.remarks.all %} @@ -103,13 +103,13 @@ <div class="card card-grey my-2"> <div class="card-header"> <h2>Nominations to the Editorial College</h2> - <a href="javascript:;" class="btn btn-secondary" data-toggle="toggle" data-target="#submitNominationForm">Nominate an Editorial Fellow candidate</a> + <a href="javascript:;" class="btn btn-outline-secondary" data-toggle="toggle" data-target="#submitNominationForm">Nominate an Editorial Fellow candidate</a> </div> <div class="card-body" style="display: none;" id="submitNominationForm"> <form action="{% url 'virtualmeetings:nominate_Fellow' VGM_id=VGM.id %}" method="post"> {% csrf_token %} {{ nomination_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"/> + <input class="btn btn-outline-secondary" type="submit" value="Submit"/> </form> </div> </div> @@ -203,12 +203,12 @@ </div> </div> <div class="card-body bg-white"> - <a href="javascript:;" class="card-link btn btn-secondary" data-toggle="toggle" data-target="#remarkForm{{ nomination.id }}">Add a remark on this Nomination</a> + <a href="javascript:;" class="card-link btn btn-outline-secondary" data-toggle="toggle" data-target="#remarkForm{{ nomination.id }}">Add a remark on this Nomination</a> <div class="submitRemarkForm my-3" id="remarkForm{{ nomination.id }}" style="display: none;"> <form action="{% url 'virtualmeetings:add_remark_on_nomination' VGM_id=VGM.id nomination_id=nomination.id %}" method="post"> {% csrf_token %} {{ remark_form|bootstrap:'0,12' }} - <input type="submit" class="btn btn-secondary" value="Submit" /> + <input type="submit" class="btn btn-outline-secondary" value="Submit" /> </form> </div> </div> @@ -241,13 +241,13 @@ <div class="card card-grey my-2"> <div class="card-header"> <h2>Submit a new Motion</h2> - <a href="javascript:;" class="btn btn-secondary" data-toggle="toggle" data-target="#submitMotionForm">Put a new Motion forward</a> + <a href="javascript:;" class="btn btn-outline-secondary" data-toggle="toggle" data-target="#submitMotionForm">Put a new Motion forward</a> </div> <div class="card-body" style="display: none;" id="submitMotionForm"> <form action="{% url 'virtualmeetings:put_motion_forward' VGM_id=VGM.id %}" method="post"> {% csrf_token %} {{ motion_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"/> + <input class="btn btn-outline-secondary" type="submit" value="Submit"/> </form> </div> </div> @@ -291,12 +291,12 @@ <strong>(you have voted: Disagree)</strong> {% endif %} </div> - <a class="btn btn-secondary" href="javascript:;" data-toggle="toggle" data-target="#remarkForm{{ motion.id }}">Add a remark on this Motion</a> + <a class="btn btn-outline-secondary" href="javascript:;" data-toggle="toggle" data-target="#remarkForm{{ motion.id }}">Add a remark on this Motion</a> <div class="submitRemarkForm mt-3" id="remarkForm{{ motion.id }}" style="display: none;"> <form action="{% url 'virtualmeetings:add_remark_on_motion' motion_id=motion.id %}" method="post"> {% csrf_token %} {{ remark_form|bootstrap:"0,12" }} - <input type="submit" class="btn btn-secondary" value="Submit" /> + <input type="submit" class="btn btn-outline-secondary" value="Submit" /> </form> </div> {% if motion.remarks.all %} diff --git a/webpack.config.js b/webpack.config.js index 6a9275d4de51bfd576ea29122e1eb64da64ec829..e9557b5bc69f0cb14559e5e8441f359e1f026abb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -54,15 +54,6 @@ module.exports = { jQuery: 'jquery', Tether: 'tether', 'window.Tether': 'tether', - // Alert: 'exports-loader?Alert!bootstrap/js/dist/alert', - // Button: 'exports-loader?Button!bootstrap/js/dist/button', - // Carousel: 'exports-loader?Carousel!bootstrap/js/dist/carousel', - // Collapse: 'exports-loader?Collapse!bootstrap/js/dist/collapse', - // Dropdown: 'exports-loader?Dropdown!bootstrap/js/dist/dropdown', - // Modal: 'exports-loader?Modal!bootstrap/js/dist/modal', - // Popover: 'exports-loader?Popover!bootstrap/js/dist/popover', - // Scrollspy: 'exports-loader?Scrollspy!bootstrap/js/dist/scrollspy', - // Tab: 'exports-loader?Tab!bootstrap/js/dist/tab', Tooltip: "exports-loader?Tooltip!bootstrap/js/dist/tooltip", Util: 'exports-loader?Util!bootstrap/js/dist/util', Popper: ['popper.js', 'default'],