diff --git a/.annotation_safe_list.yml b/.annotation_safe_list.yml deleted file mode 100644 index 62eaaa7..0000000 --- a/.annotation_safe_list.yml +++ /dev/null @@ -1,41 +0,0 @@ -# This is a Code Annotations automatically-generated Django model safelist file. -# These models must be annotated as follows in order to be counted in the coverage report. -# See https://code-annotations.readthedocs.io/en/latest/safelist.html for more information. -# -# fake_app_1.FakeModelName: -# ".. no_pii:": "This model has no PII" -# fake_app_2.FakeModel2: -# ".. choice_annotation:": foo, bar, baz - -admin.LogEntry: - ".. no_pii:": "This model has no PII" -auth.Group: - ".. no_pii:": "This model has no PII" -auth.Permission: - ".. no_pii:": "This model has no PII" -auth.User: - ".. pii": "This model minimally contains a username, password, and email" - ".. pii_types": "username, email_address, password" - ".. pii_retirement": "consumer_api" -contenttypes.ContentType: - ".. no_pii:": "This model has no PII" -sessions.Session: - ".. no_pii:": "This model has no PII" -social_django.Association: - ".. no_pii:": "This model has no PII" -social_django.Code: - ".. pii:": "Email address" - ".. pii_types:": other - ".. pii_retirement:": local_api -social_django.Nonce: - ".. no_pii:": "This model has no PII" -social_django.Partial: - ".. no_pii:": "This model has no PII" -social_django.UserSocialAuth: - ".. no_pii:": "This model has no PII" -waffle.Flag: - ".. no_pii:": "This model has no PII" -waffle.Sample: - ".. no_pii:": "This model has no PII" -waffle.Switch: - ".. no_pii:": "This model has no PII" diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ff7a9ec..0000000 --- a/.coveragerc +++ /dev/null @@ -1,10 +0,0 @@ -[run] -branch = True -data_file = .coverage -source=shoppingcart -omit = - test_settings - *migrations* - *admin.py - *static* - *templates* diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..451b749 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Description + +Describe what this pull request changes, and why. Include implications for people using this change. + +## Supporting information + +Link to other information about the change, such as Jira issues, GitHub issues, or Discourse discussions. +Be sure to check they are publicly readable, or if not, repeat the information here. + +## Testing instructions + +Please provide detailed step-by-step instructions for testing this change. + +## Deadline + +"None" if there's no rush, or provide a specific date or event (and reason) if there is one. + +## Other information + +Include anything else that will help reviewers and consumers understand the change. + +- Does this change depend on other changes elsewhere? +- Any special concerns or limitations? For example: deprecations, migrations, security, or accessibility. +- If your [database migration](https://openedx.atlassian.net/wiki/spaces/AC/pages/23003228/Everything+About+Database+Migrations) can't be rolled back easily. + +## Pre-merge checklist: + +- [ ] `make format` has been run +- [ ] This has been manually installed in a Tutor devstack and: + - [ ] manual testing steps have been followed (as described in ./docs/manual-test-plan.md) + - [ ] `make test_migrations` has been run diff --git a/.gitignore b/.gitignore index c17c528..a80dbe0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ docs/appsembler_api.*.rst # Private requirements requirements/private.in requirements/private.txt + +# cookie file for manual tests +csrf_cookies.txt diff --git a/.pii_annotations.yml b/.pii_annotations.yml deleted file mode 100644 index 2b710e0..0000000 --- a/.pii_annotations.yml +++ /dev/null @@ -1,35 +0,0 @@ -source_path: ./ -report_path: pii_report -safelist_path: .annotation_safe_list.yml -coverage_target: 100.0 -annotations: - ".. no_pii:": - "pii_group": - - ".. pii:": - - ".. pii_types:": - choices: - - id # Unique identifier for the user which is shared across systems - - name # Used for any part of the user’s name - - username - - password - - location # Used for any part of any type address or country stored - - phone_number # Used for phone or fax numbers - - email_address - - birth_date # Used for any part of a stored birth date - - ip # IP address - - external_service # Used for external service ids or links such as social media links or usernames, website links, etc. - - biography # Any type of free-form biography field - - gender - - sex - - image - - video - - other - - ".. pii_retirement:": - choices: - - retained # Intentionally kept for legal reasons - - local_api # An API exists in this repository for retiring this information - - consumer_api # The data's consumer must implement an API for retiring this information - - third_party # A third party API exists to retire this data -extensions: - python: - - py diff --git a/LICENSE.txt b/LICENSE.txt index 8b13789..1eb391f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1 +1,671 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + +EdX Inc. wishes to state, in clarification of the above license terms, that +any public, independently available web service offered over the network and +communicating with edX's copyrighted works by any form of inter-service +communication, including but not limited to Remote Procedure Call (RPC) +interfaces, is not a work based on our copyrighted work within the meaning +of the license. "Corresponding Source" of this work, or works based on this +work, as defined by the terms of this license do not include source code +files for programs used solely to provide those public, independently +available web services. diff --git a/Makefile b/Makefile index 27b5b5c..f27a04d 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,20 @@ -.PHONY: clean coverage diff_cover pii_check help quality requirements selfcheck test test-all upgrade validate test_migrations format +.PHONY: clean help quality requirements selfcheck upgrade test_migrations format .DEFAULT_GOAL := help -# For opening files in a browser. Use like: $(BROWSER)relative/path/to/file.html -BROWSER := python -m webbrowser file://$(CURDIR)/ - help: ## display this help message @echo "Please use \`make ' where is one of" @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -clean: ## remove generated byte code, coverage reports, and build artifacts +clean: ## remove generated byte code, build artifacts, etc. find . -name '__pycache__' -exec rm -rf {} + find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + - coverage erase rm -fr build/ rm -fr dist/ rm -fr *.egg-info -coverage: clean ## generate and view HTML coverage report - pytest --cov-report html - $(BROWSER)htmlcov/index.html - # Define PIP_COMPILE_OPTS=-v to get more information during make upgrade. PIP_COMPILE = pip-compile --rebuild --upgrade $(PIP_COMPILE_OPTS) @@ -32,40 +24,22 @@ upgrade: ## update the requirements/*.txt files with the latest packages satisfy # Make sure to compile files after any other files they include! $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in $(PIP_COMPILE) -o requirements/base.txt requirements/base.in - $(PIP_COMPILE) -o requirements/test.txt requirements/test.in $(PIP_COMPILE) -o requirements/quality.txt requirements/quality.in $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in - # Let tox control the Django version for tests - sed '/^[dD]jango==/d' requirements/test.txt > requirements/test.tmp - mv requirements/test.tmp requirements/test.txt -quality: selfcheck ## check coding style with pycodestyle and pylint +quality: selfcheck ## run static code quality checks tox -e quality -pii_check: ## check for PII annotations on all Django models - tox -e pii_check - requirements: ## install development environment requirements pip install -qr requirements/pip-tools.txt pip-sync requirements/dev.txt requirements/private.* -test: clean ## run tests in the current virtualenv - pytest - -diff_cover: test ## find diff lines that need test coverage - diff-cover coverage.xml - -test-all: quality ## run tests on every supported Python/Django combination - tox - -validate: quality test ## run tests and quality checks - selfcheck: ## check that the Makefile is well-formed @echo "The Makefile is well-formed." +# NOTE: This needs to be run in an tutor dev environment test_migrations: ## check that Django migrations reflect all model changes - tox -e migrations + python ./manage.py makemigrations shoppingcart --check --dry-run --verbosity 3 format: ## apply standard code formatting - isort shoppingcart setup.py manage.py tests test_utils test_settings.py - black shoppingcart setup.py manage.py tests test_utils test_settings.py + tox -e format diff --git a/README.md b/README.md index 58be93b..4b5f83c 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,54 @@ Updated for standalone, Juniper/Py3 by @bryanlandia See [apidocs.md](./appsembler_api/apidocs.md) for details on usage/the API +## Installing in a devstack + +This assumes a [Tutor dev devstack](https://docs.tutor.edly.io/dev.html) set up. + +``` +git clone https://github.com/open-craft/legacy-appsembler-api +tutor mounts add ./legacy-appsembler-api + +# This directory won't be autodetected for build-time mount by Tutor, +# so we need to manually configure Tutor for it. +TUTOR_PLUGINS_ROOT="$(tutor plugins printroot)" +mkdir -p "$TUTOR_PLUGINS_ROOT" +echo 'from tutor import hooks; hooks.Filters.MOUNTED_DIRECTORIES.add_item(("openedx", "legacy-appsembler-api"))' > "$TUTOR_PLUGINS_ROOT/legacy-appsembler-api.py" +tutor plugins enable legacy-appsembler-api + +tutor images build openedx-dev +tutor dev launch +``` + +## Developing + +Before committing, be sure to run the code formatter: + +``` +make format +``` + ## Testing -For some tests you need to run them from within a working Open edX environment. -Install in a Tutor devstack, then shell into the lms: +### Quality lints + +Quality tests (runs within tox, so you can run it directly on your workstation): + +``` +make quality +``` + +### Manual integration tests + +There is also a manual test plan to follow at [./docs/manual-test-plan.md](./docs/manual-test-plan.md). +Install this in a Tutor devstack following the instructions above, then you can run through the test plan. + +### Migration tests + +For some migration tests, +you need to run them from within a working Open edX environment. + +Install in a Tutor devstack following the instructions above, then shell into the lms: ```sh tutor dev exec lms -- bash @@ -32,9 +76,22 @@ In the LMS shell, cd to the plugin directory: cd /mnt/legacy-appsembler-api ``` -Then you can run the unit tests (no unit tests yet): +Then you can run the migrations test: + +``` +make test_migrations +``` + +From this environment, you can also: ```sh -pytest -python ./manage.py makemigrations shoppingcart --check --dry-run --verbosity 3 +# upgrade dependencies +make upgrade + +# make migrations +python ./manage.py makemigrations shoppingcart ``` + +## License + +This repository is licensed under AGPLv3. Please see [LICENSE.txt](./LICENSE.txt) for more information. diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index 4da4768..0000000 --- a/codecov.yml +++ /dev/null @@ -1,12 +0,0 @@ -coverage: - status: - project: - default: - enabled: yes - target: auto - patch: - default: - enabled: yes - target: 100% - -comment: false diff --git a/shoppingcart/apidocs.md b/docs/apidocs.md similarity index 100% rename from shoppingcart/apidocs.md rename to docs/apidocs.md diff --git a/docs/manual-test-plan.md b/docs/manual-test-plan.md new file mode 100644 index 0000000..bfe2983 --- /dev/null +++ b/docs/manual-test-plan.md @@ -0,0 +1,828 @@ +# Manual test plan + +This is a playbook to follow to manually test the api endpoints provided by this plugin. +This may also be useful as a reference for development. + +It's written to be able to follow from top to bottom. +Some steps may use outputs from previous steps. + +Prerequisites: + +- This plugin installed in an Open edX environment. + Follow the instructions in the README to install in a Tutor devstack if needed. + The instructions here assume this is running in a local Tutor devstack. +- A superuser account to use for accessing the Django admin and calling the API endpoints. + You can create one quickly via Tutor: + ```sh + tutor dev do createuser --staff --superuser user1 user1@example.com --password password + ``` +- An active course on the platform - the instructions below assume the default demo course is available. + With Tutor, you can import the demo course with: + ```sh + tutor dev do importdemocourse + ``` +- A Bash-like shell, `curl`, and `jq` installed. + +See also the [apidocs](./apidocs.md) for reference. +They may be a little outdated, but they give the general idea. + +## Create an OAuth application + +To start, you'll need to register an OAuth application in Open edX, +so you can authentication to the API. + +1. As your superuser user, go to http://local.openedx.io:8000/admin/oauth2_provider/application/ (Django OAuth Toolkit > Applications) +2. Click "Add Application" +3. Choose your previously created superuser user for the user. +4. Choose `Confidential` Client Type +5. Choose `Client Credentials` for the Authorization Grant Type +6. Set a name for your application. +7. Save the "Client id" and "Client secret" for later. + +## Retrieve a Bearer token + +```sh +CLIENT_ID="the-client-id" +CLIENT_SECRET="the-client-secret" + +curl -X POST 'http://local.openedx.io:8000/oauth2/access_token' \ + --header 'Content-Type: multipart/form-data' \ + --form "client_id=$CLIENT_ID" \ + --form "client_secret=$CLIENT_SECRET" \ + --form "token_type=bearer" \ + --form "grant_type=client_credentials" | jq +``` + +The output will look something like this: + +```json +{ + "access_token": "DlfY7WqGjwQre7TbyMBIvmIYazgFHE", + "expires_in": 36000, + "token_type": "Bearer", + "scope": "read write email profile" +} +``` + +Save the `access_token` value for use in testing the legacy-appsembler-api endpoints. + +```sh +BEARER_TOKEN="the-access-token" +``` + +## Retrieve a CSRF token + +Some API endpoints require a CSRF token. You can generate one with: + +```sh +curl -X GET 'http://local.openedx.io:8000/csrf/api/v1/token' \ + --cookie-jar csrf_cookies.txt | jq +``` + +The output will look like: + +```json +{ + "csrfToken": "WN33qvXlUEaz0GIvEinB2Df2JUtpeCr4flwN9wq3ngE1xdsyklq69A5f9waLBFz5" +} +``` + +Save the value of `csrfToken` for later: + +```sh +CSRF_TOKEN="the-csrf-token" +``` + +This also saves cookies, which includes the CSRF token cookie. +Some endpoints require the cookie, while others may be happy with the `X-CSRFToken` header. + +## Test the legacy-appsembler-api endpoints + +Now you can test the functionality of this plugin! + +You may find [./yaak-workspace.json](./yaak-workspace.json) helpful +if you use [Yaak](https://yaak.app/), a graphical API testing client. +This json file contains an export of a workspace with example requests +configured for all the relevant API endpoints, the auth API calls from above, +and request chaining to easily use them all together. + +Either way, the requests provided below for testing are formatted as curl commands that can be copy/pasted. + +### Create a user + +Example: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/create' \ + --header 'Content-Type: application/json' \ + --data '{ + "username": "apicreated4", + "password": "mypassword", + "email": "apicreated4@example.com", + "name": "my name", + "send_activation_email": "False" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Expect a json response with with status 200, that looks like: + +```json +{ + "user_id ": 5 +} +``` + +### Use the custom login endpoint + +This endpoint is a wrapper around the built-in `login_session` endpoint. +It sets a CSRF cookie that can be used cross-domain - see [CsrfCrossDomainCookieMiddleware](https://github.com/openedx/edx-platform/blob/open-release/sumac.master/openedx/core/djangoapps/cors_csrf/middleware.py#L85-L89) for more information on configuring that feature. + +Requests to this endpoint require both the CSRF token cookie *and* the CSRF token header. +If you see CSRF errors in the response, regenerate the CSRF token cookie and header by repeating +the "Retrieve a CSRF token" step from earlier. + +Test it by logging in: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/account/login_session/' \ + --cookie csrf_cookies.txt \ + --header "x-CSRFToken: $CSRF_TOKEN" \ + --header 'Content-Type: multipart/form-data' \ + --form 'email=apicreated4@example.com' \ + --form 'password=mypassword' | jq +``` + +Expect a json response with status 200 and the following body: + +```json +{ + "success": true, + "redirect_url": "http://local.openedx.io:8000/dashboard" +} +``` + +Try again with an incorrect password. +First, repeat the "Retrieve a CSRF token" step from earlier to generate a new CSRF token, +then you can run: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/account/login_session/' \ + --cookie csrf_cookies.txt \ + --header "x-CSRFToken: $CSRF_TOKEN" \ + --header 'Content-Type: multipart/form-data' \ + --form 'email=apicreated4@example.com' \ + --form 'password=notmypassword' +``` + +And verify you get a response with status 400 and the following json body: + +```json +{ + "success": false, + "value": "Email or password is incorrect.", + "error_code": "incorrect-email-or-password", + "context": { + "failure_count": 0 + }, + "email": "apicreated4@example.com" +} +``` + +### Create a user without a password + +This endpoint takes an email address and a name (and optionally some other user details), +and creates the user. +It generates a username based on the email address. + +NOTE: This endpoint has a known bug where it messes up the authentication cookies. +You shouldn't encounter this when using Curl, +but if you use an API client that tracks cookies, please clear cookies after each request here. + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/user_without_password' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "user-without-password@example.com", + "name": "Name 1" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and the following json body: + +```json +{ + "user_id": 7, + "username": "userwithoutpassword" +} +``` + +Now try again with a subtly different email address to check that it can generate unique usernames: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/user_without_password' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "user--without-password@example.com", + "name": "Name 2" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200, and a json body that looks like this +(the trailing digits on the username may be different): + +```json +{ + "user_id": 9, + "username": "userwithoutpassword732" +} +``` + +Finally, try again with the same email address to verify it blocks duplicate emails with a custom response code: + +```sh +curl -w "%{stderr}%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/user_without_password' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "user--without-password@example.com", + "name": "Name 2" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 409 (Conflict), and the following json body: + +```json +{ + "user_message": "User already exists" +} +``` + +### Test the connect endpoint + +Given a username, the connect endpoint can change the email, password, and name for the user. +Run this to update details for the `apicreated4` user you created in the first step to create a user: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/connect' \ + --header 'Content-Type: application/json' \ + --data '{ + "username": "apicreated4", + "password": "newpassword", + "email": "newemail@example.com", + "name": "my new name" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify you get a response with status 200 and a json body that looks like: + +```json +{ + "user_id": 5 +} +``` + +Verify you can log in to the [devstack web ui](http://local.openedx.io:8000) +with the username and new password from above. + +### Test getting a user + +This endpoint is used for checking if a user exists: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/accounts/get-user/apicreated4' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and the following json body: + +```json +{ + "user_id": "apicreated4" +} +``` + +Test with a non-existing user: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/accounts/get-user/doesnotexist' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the status code is 404. + +### Test updating a user's profile + +The update user endpoint supports updating many fields from a user's profile: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/accounts/update_user' \ + --header 'Content-Type: application/json' \ + --data '{ + "user_lookup": "apicreated4", + "email": "apicreated1@example.com", + "name": "my new name", + "year_of_birth": "2001", + "bio": "just another user", + "country": "AU", + "level_of_education": "a", + "gender": "m", + "language": "EN" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response confirms the fields have been updated: + +```json +{ + "success": "The following fields has been updated: email=apicreated1@example.com, name=my new name, level_of_education=a, gender=m, country=AU, bio=just another user, year_of_birth=2001, language=EN" +} +``` + +You can also log in to the web UI and view profile information on http://apps.local.openedx.io:1995/profile/u/apicreated4 . + +### Test search courses + +There is an endpoint to search for courses: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/search_courses?search_term=demo&org=openedx' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify this search above returns the demo course: + +```json +{ + "results": [ + { + "blocks_url": "http://local.openedx.io:8000/api/courses/v2/blocks/?course_id=course-v1%3AOpenedX%2BDemoX%2BDemoCourse", + "effort": null, + "end": null, + "enrollment_start": null, + "enrollment_end": null, + "id": "course-v1:OpenedX+DemoX+DemoCourse", + "media": { + "banner_image": { + "uri": "/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@images_course_image.jpg", + "uri_absolute": "http://local.openedx.io:8000/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@images_course_image.jpg" + }, + "course_image": { + "uri": "/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@thumbnail_demox.jpeg" + }, + "course_video": { + "uri": null + }, + "image": { + "raw": "http://local.openedx.io:8000/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@thumbnail_demox.jpeg", + "small": "http://local.openedx.io:8000/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@thumbnail_demox.jpeg", + "large": "http://local.openedx.io:8000/asset-v1:OpenedX+DemoX+DemoCourse+type@asset+block@thumbnail_demox.jpeg" + } + }, + "name": "Open edX Demo Course", + "number": "DemoX", + "org": "OpenedX", + "short_description": "Explore Open edX® capabilities in this demo course, covering platform tools, content creation, assessments, social learning, and community stories. Ideal for course developers, online learning newcomers, and community members. ", + "start": "2020-01-01T00:00:00Z", + "start_display": "Jan. 1, 2020", + "start_type": "timestamp", + "pacing": "self", + "mobile_available": false, + "hidden": false, + "invitation_only": false, + "course_id": "course-v1:OpenedX+DemoX+DemoCourse" + } + ], + "pagination": { + "next": null, + "previous": null, + "count": 1, + "num_pages": 1 + } +} +``` + +Search for something that doesn't exist: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/search_courses?search_term=demo&org=openedxnth' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response still has 200 status, but returns this json body: + +```json +{ + "results": [], + "pagination": { + "next": null, + "previous": null, + "count": 0, + "num_pages": 1 + } +} +``` + +NOTE: this endpoint also supports a `filter_` parameter for extra filtering (this is passed directly to [lms.djangoapps.course_api.api.list_courses](https://github.com/openedx/edx-platform/blob/198886d191d6cb1a73bed1260b5457dc3784f481/lms/djangoapps/course_api/api.py#L120), +and a `username` to filter by courses that are visible to that user. + +### Test bulk enrollment + + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/bulk-enrollment/bulk-enroll' \ + --header 'Content-Type: application/json' \ + --data '{ + "action": "enroll", + "auto_enroll": true, + "identifiers": "apicreated1@example.com,apicreated2@example.com", + "email_students": true, + "courses": "course-v1:OpenedX+DemoX+DemoCourse" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and shows the results: + +```json +{ + "auto_enroll": true, + "email_students": true, + "action": "enroll", + "courses": { + "course-v1:OpenedX+DemoX+DemoCourse": { + "action": "enroll", + "results": [ + { + "identifier": "apicreated1@example.com", + "before": { + "user": true, + "enrollment": false, + "allowed": false, + "auto_enroll": false + }, + "after": { + "user": true, + "enrollment": true, + "allowed": false, + "auto_enroll": false + } + }, + { + "identifier": "apicreated2@example.com", + "before": { + "user": false, + "enrollment": false, + "allowed": false, + "auto_enroll": false + }, + "after": { + "user": false, + "enrollment": false, + "allowed": true, + "auto_enroll": true + } + } + ], + "auto_enroll": true + } + } +} +``` + +Note that this works with user email address that don't exist too; +this is expected, and simply uses the edx-platform functions to perform the enrollments. +I think if a user doesn't exist, they'll receive an email inviting them to enroll. + +### Test generating enrollment codes + +This is functionality for providing a single-user code tied to a course, +allowing a user to be enrolled in that course by providing that code later. + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/generate' \ + --header 'Content-Type: application/json' \ + --data '{ + "total_registration_codes": "3", + "course_id": "course-v1:OpenedX+DemoX+DemoCourse" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status code 200 and json body with information about the course and the desired number of codes: + +```json +{ + "codes": [ + "PY4ngLKw", + "zbKy2BbR", + "RDm6VkVd" + ], + "course_id": "course-v1:OpenedX+DemoX+DemoCourse", + "course_url": "/courses/course-v1:OpenedX+DemoX+DemoCourse/about" +} +``` + +Save two codes for the next steps: + +```sh +ENROLLMENT_CODE1="YOURCODE1" +ENROLLMENT_CODE2="YOURCODE2" +``` + +### Test enrolling users with enrollment codes + +Now you can enroll a user in the demo course using one of those codes: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "apicreated1@example.com", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify a success response is returned: + +```json +{ + "success": true +} +``` + +Log in to the web UI as the "apicreated1@example.com" user +(username "apicreated4"; sorry, that's confusing; password "newpassword"), +and verify the user is enrolled in the Open edX Demo Course. + + +Test enrolling again with the same code: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "apicreated1@example.com", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +The code is single use, so verify the response has status 400, and the following json body: + +```json +{ + "success": false, + "reason": "" +} +``` + +NOTE: it may be a minor bug that the reason is empty here. + +Try again with a code that does not exist: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "apicreated1@example.com", + "enrollment_code": "doesnotexist" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 400, and the following json body: + +```json +{ + "success": false, + "reason": "Enrollment code not found" +} +``` + +Finally, test the case where the user does not exist, but the enrollment code is valid +(pick one of the other unused enrollment codes): + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "doesnotexist@example.com", + "enrollment_code": "'"$ENROLLMENT_CODE2"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 400 and the following json body: + +```json +{ + "success": false, + "reason": "User not found" +} +``` + + +### Test changing enrollment code status + +The enrollment-codes/status endpoint supports cancelling or restoring an enrollment code. + +- "cancel": invalidate the code. Also unenroll the user from the course if the code was used. +- "restore": Make the code valid and ready to use again. Also unenroll the user from the course if the code was used. + +Let's restore the enrollment code that was used earlier to enroll the "apicreated4" user: + + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/status' \ + --header 'Content-Type: application/json' \ + --data '{ + "action": "restore", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and the following json body: + +```json +{ + "success": true +} +``` + +Visit the web UI as the "apicreated4" user again, +and verify the user is no longer enrolled in the demo course. + +Enroll the user in the demo course again using the same (restored) code: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "apicreated1@example.com", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify a success response is returned: + +```json +{ + "success": true +} +``` + +In the web UI, verify the user is enrolled again in the demo course. + +Now cancel the enrollment code: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/status' \ + --header 'Content-Type: application/json' \ + --data '{ + "action": "cancel", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and the following json body: + +```json +{ + "success": true +} +``` + +In the web UI, verify the user is once again no longer enrolled in the demo course. + +One final time, attempt to enroll the user in the course using the same code: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X POST 'http://local.openedx.io:8000/appsembler_api/v0/enrollment-codes/enroll-user' \ + --header 'Content-Type: application/json' \ + --data '{ + "email": "apicreated1@example.com", + "enrollment_code": "'"$ENROLLMENT_CODE1"'" +}' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 400 and the following json body: + +```json +{ + "success": false, + "reason": "Enrollment code not found" +} +``` + +In the web UI, verify the user is still not enrolled in the demo course. + +### Test the accounts analytics endpoint + +This shows information about user accounts, and can filter by date joined. + +You can get a list of all the users on the platform with: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/analytics/accounts/batch' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and the body has a json list of all users: + +```json5 +[ + { + "id": 2, + "username": "login_service_user", + "email": "login_service_user@fake.email", + "is_active": true, + "date_joined": "2025-09-29T06:35:08.751215Z" + }, + { + "id": 3, + "username": "cms", + "email": "cms@openedx", + "is_active": true, + "date_joined": "2025-09-29T06:36:27.318723Z" + }, + { + "id": 5, + "username": "apicreated4", + "email": "apicreated1@example.com", + "is_active": true, + "date_joined": "2025-09-29T07:05:03.092197Z" + }, + // ... +] +``` + +You can also filter by date joined - for example (adjust the dates to suite when you run this): + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/analytics/accounts/batch?updated_max=2025-10-03&updated_min=2025-09-29' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response looks similar to the to the previous response, +but this time has only users who joined in the provided date range. + +### Test the enrollments analytics endpoint + +This endpoint returns information about enrollments, +and you can filter by `course_id`, `username`, `updated_min` (date), and `updated_max` (date). + +For example to get all enrollments in the demo course: + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/analytics/enrollment/batch?course_id=course-v1%3AOpenedX%2BDemoX%2BDemoCourse' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response has status 200 and a json body that looks like: + +```json +[ + { + "enrollment_id": 1, + "user_id": 5, + "username": "apicreated4", + "course_id": "course-v1:OpenedX+DemoX+DemoCourse", + "date_enrolled": "2025-09-30T01:00:45.011099Z" + } +] +``` + +An example that uses all the filters (modify as desired to test for applicable dates and usernames): + +```sh +curl -w "%{stderr}\nResponse code: %{http_code}\n" -X GET 'http://local.openedx.io:8000/appsembler_api/v0/analytics/enrollment/batch?updated_max=2031-01-01&updated_min=2025-09-01&username=apicreated4&course_id=course-v1%3AOpenedX%2BDemoX%2BDemoCourse' \ + --header 'Content-Type: application/json' \ + --data '' \ + --header "Authorization: Bearer $BEARER_TOKEN" | jq +``` + +Verify the response returns results as expected. diff --git a/docs/yaak-workspace.json b/docs/yaak-workspace.json new file mode 100644 index 0000000..76eb75b --- /dev/null +++ b/docs/yaak-workspace.json @@ -0,0 +1,739 @@ +{ + "yaakVersion": "0.0.0", + "yaakSchema": 4, + "timestamp": "2025-09-29T06:35:37.894018172", + "resources": { + "workspaces": [ + { + "model": "workspace", + "id": "wk_APy3xzDHqt", + "createdAt": "2025-09-08T02:29:52.878532497", + "updatedAt": "2025-09-17T00:32:25.993042473", + "authentication": {}, + "authenticationType": null, + "description": "", + "headers": [], + "name": "Open edX", + "encryptionKeyChallenge": null, + "settingValidateCertificates": true, + "settingFollowRedirects": true, + "settingRequestTimeout": 0 + } + ], + "environments": [ + { + "model": "environment", + "id": "ev_AY45iYsRut", + "workspaceId": "wk_APy3xzDHqt", + "createdAt": "2025-09-08T02:29:54.905785784", + "updatedAt": "2025-09-17T00:39:25.907154243", + "name": "Global Variables", + "public": false, + "base": true, + "variables": [ + { + "enabled": true, + "name": "CLIENT_ID", + "value": "api-testing-key", + "id": "N46MgQreR5" + }, + { + "enabled": true, + "name": "CLIENT_SECRET", + "value": "api-testing-secret", + "id": "376ESvnEjQ" + }, + { + "enabled": true, + "name": "LMS", + "value": "http://local.openedx.io:8000", + "id": "cRUiMGJEFn" + }, + { + "enabled": true, + "name": "STUDIO", + "value": "http://studio.local.openedx.io:8001", + "id": "pphdqhruDm" + } + ], + "color": null + } + ], + "folders": [ + { + "model": "folder", + "id": "fl_Ry6DjUQEqZ", + "createdAt": "2025-09-17T00:36:36.228936194", + "updatedAt": "2025-09-17T02:26:41.937860825", + "workspaceId": "wk_APy3xzDHqt", + "folderId": null, + "authentication": { + "token": "${[ response.body.path(request='rq_iep7QXuiyh', path=b64'JC5hY2Nlc3NfdG9rZW4', behavior='always') ]}" + }, + "authenticationType": "bearer", + "description": "", + "headers": [], + "name": "legacy-appsembler-api", + "sortPriority": 1000.0 + }, + { + "model": "folder", + "id": "fl_TmgykaqU4a", + "createdAt": "2025-09-08T02:33:11.740254732", + "updatedAt": "2025-09-17T00:43:26.786439583", + "workspaceId": "wk_APy3xzDHqt", + "folderId": null, + "authentication": {}, + "authenticationType": null, + "description": "Auth views - you will need a valid client_id and client_secret - see https://docs.openedx.org/projects/edx-platform/en/latest/how-tos/use_the_api.html", + "headers": [], + "name": "Auth", + "sortPriority": 0.0 + } + ], + "httpRequests": [ + { + "model": "http_request", + "id": "rq_5wMTMNAWhP", + "createdAt": "2025-09-19T06:29:08.064721192", + "updatedAt": "2025-09-22T03:45:35.553346245", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"action\": \"restore\",\n \"enrollment_code\": \"2q0zb35r\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": false, + "name": "x-CSRFToken", + "value": "${[ response.body.path(request='rq_WrGRLPnEmq', path=b64'JC5jc3JmVG9rZW4', behavior='always') ]}", + "id": "2G4vBr8seA" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "5bjegf4Xd0" + } + ], + "method": "POST", + "name": "enrollment-codes/status", + "sortPriority": 11000.001, + "url": "${[ LMS ]}/appsembler_api/v0/enrollment-codes/status", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_Sb6ppk5vdH", + "createdAt": "2025-09-19T06:04:20.735338763", + "updatedAt": "2025-09-22T03:45:27.010848821", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"email\": \"apicreated2@example.com\",\n \"enrollment_code\": \"2q0zb35r\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": false, + "name": "x-CSRFToken", + "value": "${[ response.body.path(request='rq_WrGRLPnEmq', path=b64'JC5jc3JmVG9rZW4', behavior='always') ]}", + "id": "2G4vBr8seA" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "5bjegf4Xd0" + } + ], + "method": "POST", + "name": "enrollment-codes/enroll-user", + "sortPriority": 11000.0, + "url": "${[ LMS ]}/appsembler_api/v0/enrollment-codes/enroll-user", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_Z8T2cqxK8C", + "createdAt": "2025-09-19T05:54:05.733450592", + "updatedAt": "2025-09-19T07:00:59.322514964", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"total_registration_codes\": \"3\",\n \"course_id\": \"course-v1:OpenedX+DemoX+DemoCourse\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": false, + "name": "x-CSRFToken", + "value": "${[ response.body.path(request='rq_WrGRLPnEmq', path=b64'JC5jc3JmVG9rZW4', behavior='always') ]}", + "id": "2G4vBr8seA" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "TABRa20ev8" + } + ], + "method": "POST", + "name": "enrollment-codes/generate", + "sortPriority": 10000.0, + "url": "${[ LMS ]}/appsembler_api/v0/enrollment-codes/generate", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_X4KWPccTd8", + "createdAt": "2025-09-19T05:07:43.908000793", + "updatedAt": "2025-09-22T03:46:31.581356770", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "GET", + "name": "analytics/enrollment/batch", + "sortPriority": 9000.0, + "url": "${[ LMS ]}/appsembler_api/v0/analytics/enrollment/batch", + "urlParameters": [ + { + "enabled": true, + "name": "updated_max", + "value": "2025-09-23", + "id": "eCFjIjVR4C" + }, + { + "enabled": true, + "name": "updated_min", + "value": "2025-09-01", + "id": "CB8Hz9YMIt" + }, + { + "enabled": false, + "name": "username", + "value": "samuel", + "id": "pEKBTHX9kj" + }, + { + "enabled": true, + "name": "course_id", + "value": "course-v1:OpenedX+DemoX+DemoCourse", + "id": "c9FFSMNdTF" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "2uk0nKRpZz" + } + ] + }, + { + "model": "http_request", + "id": "rq_YfHzvBHz5Z", + "createdAt": "2025-09-19T05:04:10.824706583", + "updatedAt": "2025-09-22T03:45:55.111010142", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "GET", + "name": "analytics/accounts/batch", + "sortPriority": 8000.0, + "url": "${[ LMS ]}/appsembler_api/v0/analytics/accounts/batch", + "urlParameters": [ + { + "enabled": true, + "name": "updated_max", + "value": "2025-09-23", + "id": "eCFjIjVR4C" + }, + { + "enabled": true, + "name": "updated_min", + "value": "2025-09-01", + "id": "CB8Hz9YMIt" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "bxtU4NmFfc" + } + ] + }, + { + "model": "http_request", + "id": "rq_92u3uoRksM", + "createdAt": "2025-09-19T04:54:46.980951012", + "updatedAt": "2025-09-19T06:05:41.924590927", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "GET", + "name": "search_courses", + "sortPriority": 7000.0, + "url": "${[ LMS ]}/appsembler_api/v0/search_courses", + "urlParameters": [ + { + "enabled": true, + "name": "search_term", + "value": "demo", + "id": "6YEfqkfiex" + }, + { + "enabled": true, + "name": "org", + "value": "openedx", + "id": "8ReJTVkUin" + }, + { + "enabled": true, + "name": "filter_", + "value": "foo=yes", + "id": "RsqNu8qPr3" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "U0Tq7GTVy2" + } + ] + }, + { + "model": "http_request", + "id": "rq_JfyE4VB7rh", + "createdAt": "2025-09-19T04:47:35.118800954", + "updatedAt": "2025-09-19T06:05:41.780598678", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"email\": \"testuser11157161811@example.com\",\n \"name\": \"Name 1\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "POST", + "name": "accounts/user_without_password", + "sortPriority": 2000.0, + "url": "${[ LMS ]}/appsembler_api/v0/accounts/user_without_password", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_TwtQEarWBA", + "createdAt": "2025-09-17T06:18:33.093744862", + "updatedAt": "2025-09-19T06:05:41.718975790", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": "none", + "body": { + "form": [ + { + "enabled": true, + "id": "uG6FbaGx8t", + "name": "email", + "value": "apicreated2@example.com" + }, + { + "enabled": true, + "id": "hcxpRvYc64", + "name": "password", + "value": "mypassword" + }, + { + "enabled": true, + "id": "aSdiTsGIE3", + "name": "", + "value": "" + } + ] + }, + "bodyType": "multipart/form-data", + "description": "", + "headers": [ + { + "enabled": true, + "name": "x-CSRFToken", + "value": "${[ response.body.path(request='rq_WrGRLPnEmq', path=b64'JC5jc3JmVG9rZW4', behavior='always') ]}", + "id": "RA4ymWWJWS" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "mzgkqdCaDu" + }, + { + "enabled": true, + "name": "Content-Type", + "value": "multipart/form-data", + "id": "0YfTYCAWkT" + } + ], + "method": "POST", + "name": "account/login_session", + "sortPriority": 0.0, + "url": "${[ LMS ]}/appsembler_api/v0/account/login_session/", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_exDSx2qZ4L", + "createdAt": "2025-09-17T02:43:25.304427839", + "updatedAt": "2025-09-19T06:05:41.871658542", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"user_lookup\": \"apicreated1\",\n \"email\": \"apicreated1@example.com\",\n \"name\": \"my new name\",\n \"year_of_birth\": \"2001\",\n \"bio\": \"just another user\",\n \"country\": \"AU\",\n \"level_of_education\": \"a\",\n \"gender\": \"m\",\n \"language\": \"EN\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "POST", + "name": "accounts/update_user", + "sortPriority": 5000.0, + "url": "${[ LMS ]}/appsembler_api/v0/accounts/update_user", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_HkUu5eTgTt", + "createdAt": "2025-09-17T02:37:10.339763102", + "updatedAt": "2025-09-19T06:05:41.819270050", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"username\": \"apicreated1\",\n \"password\": \"newpassword\",\n \"email\": \"apicreated1@example.com\",\n \"name\": \"my new name\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "POST", + "name": "accounts/connect", + "sortPriority": 3000.0, + "url": "${[ LMS ]}/appsembler_api/v0/accounts/connect", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_UMrw3Z9wVC", + "createdAt": "2025-09-17T02:31:08.676850320", + "updatedAt": "2025-09-19T06:05:41.897848344", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"action\": \"enroll\",\n \"auto_enroll\": true,\n \"identifiers\": \"apicreated1@example.com,apicreated2@example.com\",\n \"email_students\": true,\n \"courses\": \"course-v1:OpenedX+DemoX+DemoCourse\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "POST", + "name": "bulk-enrollment/bulk-enroll", + "sortPriority": 6000.0, + "url": "${[ LMS ]}/appsembler_api/v0/bulk-enrollment/bulk-enroll", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_reDAowFSrk", + "createdAt": "2025-09-17T02:25:54.113660987", + "updatedAt": "2025-09-23T06:26:48.284800449", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "GET", + "name": "accounts/get-user", + "sortPriority": 4000.0, + "url": "${[ LMS ]}/appsembler_api/v0/accounts/get-user/apicreated3", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_JHb8U6LVsM", + "createdAt": "2025-09-17T00:37:49.096406570", + "updatedAt": "2025-09-26T06:01:24.148454640", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_Ry6DjUQEqZ", + "authentication": {}, + "authenticationType": null, + "body": { + "text": "{\n \"username\": \"apicreated4\",\n \"password\": \"mypassword\",\n \"email\": \"apicreated4@example.com\",\n \"name\": \"my name\",\n \"send_activation_email\": \"False\"\n}" + }, + "bodyType": "application/json", + "description": "", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "application/json", + "id": "VI2e5F69aI" + }, + { + "enabled": true, + "name": "", + "value": "", + "id": "nKhzb2UWXK" + } + ], + "method": "POST", + "name": "accounts/create", + "sortPriority": 1000.0, + "url": "${[ LMS ]}/appsembler_api/v0/accounts/create", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_iep7QXuiyh", + "createdAt": "2025-09-11T00:38:29.639923184", + "updatedAt": "2025-09-29T06:34:50.640564494", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_TmgykaqU4a", + "authentication": {}, + "authenticationType": "none", + "body": { + "form": [ + { + "enabled": true, + "id": "HZqsUyY2Gh", + "name": "client_id", + "value": "${[ CLIENT_ID ]}" + }, + { + "enabled": true, + "id": "aj8JzB3HGu", + "name": "client_secret", + "value": "${[ CLIENT_SECRET ]}" + }, + { + "enabled": true, + "id": "IY8Mfzr9FZ", + "name": "token_type", + "value": "bearer" + }, + { + "enabled": true, + "id": "kT28Pe6cFR", + "name": "grant_type", + "value": "client_credentials" + }, + { + "enabled": true, + "id": "y28PNcX24D", + "name": "", + "value": "" + } + ] + }, + "bodyType": "multipart/form-data", + "description": "requires oauth2 provider set up from http://local.openedx.io:8000/admin/oauth2_provider/application/\n\nSee also https://discuss.openedx.org/t/authenticate-with-oauth-token-to-access-api-endpoints-instructor-apis/13658 for help", + "headers": [ + { + "enabled": true, + "name": "Content-Type", + "value": "multipart/form-data", + "id": "vcnE3JtGnA" + } + ], + "method": "POST", + "name": "Bearer token", + "sortPriority": 0.0, + "url": "${[ LMS ]}/oauth2/access_token", + "urlParameters": [] + }, + { + "model": "http_request", + "id": "rq_WrGRLPnEmq", + "createdAt": "2025-09-08T02:40:27.811491757", + "updatedAt": "2025-09-17T00:44:39.128316928", + "workspaceId": "wk_APy3xzDHqt", + "folderId": "fl_TmgykaqU4a", + "authentication": {}, + "authenticationType": "none", + "body": { + "form": [] + }, + "bodyType": null, + "description": "", + "headers": [], + "method": "GET", + "name": "CSRF token", + "sortPriority": 2000.0, + "url": "${[ LMS ]}/csrf/api/v1/token", + "urlParameters": [] + } + ], + "grpcRequests": [], + "websocketRequests": [] + } +} diff --git a/manage.py b/manage.py index f45575c..29d2ef8 100644 --- a/manage.py +++ b/manage.py @@ -3,14 +3,9 @@ Django administration utility. """ -import os import sys -PWD = os.path.abspath(os.path.dirname(__file__)) - if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") - sys.path.append(PWD) try: from django.core.management import execute_from_command_line except ImportError: diff --git a/pyproject.toml b/pyproject.toml index 55ec8d7..85c3b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,5 @@ [tool.black] line-length = 120 + +[tool.isort] +profile = "black" diff --git a/requirements/base.txt b/requirements/base.txt index f0ff9f1..2482a8d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # make upgrade # -asgiref==3.9.1 +asgiref==3.9.2 # via django django==4.2.24 # via diff --git a/requirements/dev.in b/requirements/dev.in index 4925daf..fd3c454 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -4,6 +4,5 @@ -r pip-tools.txt # pip-tools and its dependencies, for managing requirements files -r quality.txt # Core and quality check dependencies -diff-cover # Changeset diff test coverage edx-i18n-tools # For i18n_tool dummy tox-battery # Makes tox aware of requirements file changes diff --git a/requirements/dev.txt b/requirements/dev.txt index 3dd8025..a515e57 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # make upgrade # -asgiref==3.9.1 +asgiref==3.9.2 # via # -r requirements/quality.txt # django @@ -31,8 +31,6 @@ cffi==2.0.0 # via # -r requirements/quality.txt # cryptography -chardet==5.2.0 - # via diff-cover charset-normalizer==3.4.3 # via # -r requirements/quality.txt @@ -54,16 +52,10 @@ code-annotations==2.3.0 # via # -r requirements/quality.txt # edx-lint -coverage[toml]==7.10.7 - # via - # -r requirements/quality.txt - # pytest-cov cryptography==46.0.1 # via # -r requirements/quality.txt # secretstorage -diff-cover==9.6.0 - # via -r requirements/dev.in dill==0.4.0 # via # -r requirements/quality.txt @@ -102,10 +94,6 @@ importlib-metadata==8.7.0 # via # -r requirements/quality.txt # keyring -iniconfig==2.1.0 - # via - # -r requirements/quality.txt - # pytest isort==6.0.1 # via # -r requirements/quality.txt @@ -131,7 +119,6 @@ jinja2==3.1.6 # via # -r requirements/quality.txt # code-annotations - # diff-cover keyring==25.6.0 # via # -r requirements/quality.txt @@ -146,7 +133,7 @@ markdown-it-py==4.0.0 # via # -r requirements/quality.txt # rich -markupsafe==3.0.2 +markupsafe==3.0.3 # via # -r requirements/quality.txt # jinja2 @@ -177,7 +164,6 @@ packaging==25.0 # -r requirements/quality.txt # black # build - # pytest # tox # twine path==16.16.0 @@ -195,29 +181,18 @@ platformdirs==4.4.0 # pylint # virtualenv pluggy==1.6.0 - # via - # -r requirements/quality.txt - # diff-cover - # pytest - # pytest-cov - # tox + # via tox polib==1.2.0 # via edx-i18n-tools py==1.11.0 # via tox -pycodestyle==2.14.0 - # via -r requirements/quality.txt pycparser==2.23 # via # -r requirements/quality.txt # cffi -pydocstyle==6.3.0 - # via -r requirements/quality.txt pygments==2.19.2 # via # -r requirements/quality.txt - # diff-cover - # pytest # readme-renderer # rich pylint==3.3.8 @@ -245,15 +220,6 @@ pyproject-hooks==1.2.0 # -r requirements/pip-tools.txt # build # pip-tools -pytest==8.4.2 - # via - # -r requirements/quality.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/quality.txt -pytest-django==4.11.1 - # via -r requirements/quality.txt python-slugify==8.0.4 # via # -r requirements/quality.txt @@ -262,7 +228,7 @@ pytokens==0.1.10 # via # -r requirements/quality.txt # black -pyyaml==6.0.2 +pyyaml==6.0.3 # via # -r requirements/quality.txt # code-annotations @@ -298,10 +264,6 @@ six==1.17.0 # -r requirements/quality.txt # edx-lint # tox -snowballstemmer==3.0.1 - # via - # -r requirements/quality.txt - # pydocstyle sqlparse==0.5.3 # via # -r requirements/quality.txt diff --git a/requirements/quality.in b/requirements/quality.in index 5679b41..e0787af 100644 --- a/requirements/quality.in +++ b/requirements/quality.in @@ -1,12 +1,10 @@ # Requirements for code quality checks -c constraints.txt --r test.txt # Core and testing dependencies for this package +-r base.txt # Core dependencies for this package black edx-lint # edX pylint rules and plugins isort # to standardize order of imports -pycodestyle # PEP 8 compliance validation -pydocstyle # PEP 257 compliance validation pylint twine # Utility for publishing Python packages on PyPI. diff --git a/requirements/quality.txt b/requirements/quality.txt index 57be367..b99520d 100644 --- a/requirements/quality.txt +++ b/requirements/quality.txt @@ -4,9 +4,9 @@ # # make upgrade # -asgiref==3.9.1 +asgiref==3.9.2 # via - # -r requirements/test.txt + # -r requirements/base.txt # django astroid==3.3.11 # via @@ -24,7 +24,6 @@ charset-normalizer==3.4.3 # via requests click==8.3.0 # via - # -r requirements/test.txt # black # click-log # code-annotations @@ -32,13 +31,7 @@ click==8.3.0 click-log==0.4.0 # via edx-lint code-annotations==2.3.0 - # via - # -r requirements/test.txt - # edx-lint -coverage[toml]==7.10.7 - # via - # -r requirements/test.txt - # pytest-cov + # via edx-lint cryptography==46.0.1 # via secretstorage dill==0.4.0 @@ -46,10 +39,10 @@ dill==0.4.0 django==4.2.24 # via # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/test.txt + # -r requirements/base.txt # djangorestframework djangorestframework==3.16.1 - # via -r requirements/test.txt + # via -r requirements/base.txt docutils==0.22.2 # via readme-renderer edx-lint==5.6.0 @@ -60,10 +53,6 @@ idna==3.10 # via requests importlib-metadata==8.7.0 # via keyring -iniconfig==2.1.0 - # via - # -r requirements/test.txt - # pytest isort==6.0.1 # via # -r requirements/quality.in @@ -79,17 +68,13 @@ jeepney==0.9.0 # keyring # secretstorage jinja2==3.1.6 - # via - # -r requirements/test.txt - # code-annotations + # via code-annotations keyring==25.6.0 # via twine markdown-it-py==4.0.0 # via rich -markupsafe==3.0.2 - # via - # -r requirements/test.txt - # jinja2 +markupsafe==3.0.3 + # via jinja2 mccabe==0.7.0 # via pylint mdurl==0.1.2 @@ -104,9 +89,7 @@ nh3==0.3.0 # via readme-renderer packaging==25.0 # via - # -r requirements/test.txt # black - # pytest # twine pathspec==0.12.1 # via black @@ -114,21 +97,10 @@ platformdirs==4.4.0 # via # black # pylint -pluggy==1.6.0 - # via - # -r requirements/test.txt - # pytest - # pytest-cov -pycodestyle==2.14.0 - # via -r requirements/quality.in pycparser==2.23 # via cffi -pydocstyle==6.3.0 - # via -r requirements/quality.in pygments==2.19.2 # via - # -r requirements/test.txt - # pytest # readme-renderer # rich pylint==3.3.8 @@ -146,25 +118,12 @@ pylint-plugin-utils==0.9.0 # via # pylint-celery # pylint-django -pytest==8.4.2 - # via - # -r requirements/test.txt - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/test.txt -pytest-django==4.11.1 - # via -r requirements/test.txt python-slugify==8.0.4 - # via - # -r requirements/test.txt - # code-annotations + # via code-annotations pytokens==0.1.10 # via black -pyyaml==6.0.2 - # via - # -r requirements/test.txt - # code-annotations +pyyaml==6.0.3 + # via code-annotations readme-renderer==44.0 # via twine requests==2.32.5 @@ -182,20 +141,14 @@ secretstorage==3.4.0 # via keyring six==1.17.0 # via edx-lint -snowballstemmer==3.0.1 - # via pydocstyle sqlparse==0.5.3 # via - # -r requirements/test.txt + # -r requirements/base.txt # django stevedore==5.5.0 - # via - # -r requirements/test.txt - # code-annotations + # via code-annotations text-unidecode==1.3 - # via - # -r requirements/test.txt - # python-slugify + # via python-slugify tomlkit==0.13.3 # via pylint twine==6.2.0 diff --git a/requirements/test.in b/requirements/test.in deleted file mode 100644 index 6797160..0000000 --- a/requirements/test.in +++ /dev/null @@ -1,8 +0,0 @@ -# Requirements for test runs. --c constraints.txt - --r base.txt # Core dependencies for this package - -pytest-cov # pytest extension for code coverage statistics -pytest-django # pytest extension for better Django support -code-annotations # provides commands used by the pii_check make target. diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index c114844..0000000 --- a/requirements/test.txt +++ /dev/null @@ -1,56 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# make upgrade -# -asgiref==3.9.1 - # via - # -r requirements/base.txt - # django -click==8.3.0 - # via code-annotations -code-annotations==2.3.0 - # via -r requirements/test.in -coverage[toml]==7.10.7 - # via pytest-cov - # via - # -c https:/raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt - # -r requirements/base.txt - # djangorestframework -djangorestframework==3.16.1 - # via -r requirements/base.txt -iniconfig==2.1.0 - # via pytest -jinja2==3.1.6 - # via code-annotations -markupsafe==3.0.2 - # via jinja2 -packaging==25.0 - # via pytest -pluggy==1.6.0 - # via - # pytest - # pytest-cov -pygments==2.19.2 - # via pytest -pytest==8.4.2 - # via - # pytest-cov - # pytest-django -pytest-cov==7.0.0 - # via -r requirements/test.in -pytest-django==4.11.1 - # via -r requirements/test.in -python-slugify==8.0.4 - # via code-annotations -pyyaml==6.0.2 - # via code-annotations -sqlparse==0.5.3 - # via - # -r requirements/base.txt - # django -stevedore==5.5.0 - # via code-annotations -text-unidecode==1.3 - # via python-slugify diff --git a/setup.cfg b/setup.cfg index a61eee7..5e40900 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,2 @@ -[isort] -profile = black - [wheel] universal = 1 diff --git a/setup.py b/setup.py index e0fe760..caeafb6 100755 --- a/setup.py +++ b/setup.py @@ -117,7 +117,7 @@ def is_requirement(line): long_description=README + "\n\n" + CHANGELOG, author="Appsembler, Inc.", author_email="john@appsembler.com", - url="https://github.com/appsembler/legacy-appsembler-api", + url="https://github.com/open-craft/legacy-appsembler-api", packages=[ "shoppingcart", ], @@ -129,12 +129,10 @@ def is_requirement(line): classifiers=[ "Development Status :: 3 - Alpha", "Framework :: Django", - "Framework :: Django :: 2.2", "Intended Audience :: Developers", - "License :: Other/Proprietary License", + "License :: OSI Approved :: GNU Affero General Public License v3", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", ], entry_points={"lms.djangoapp": ["shoppingcart = shoppingcart.apps:AppsemblerApiConfig"]}, ) diff --git a/shoppingcart/utils.py b/shoppingcart/utils.py index c345247..a1cf3e8 100644 --- a/shoppingcart/utils.py +++ b/shoppingcart/utils.py @@ -80,7 +80,7 @@ def send_activation_email(request): return False -def get_reg_code_validity(registration_code, request): +def get_reg_code_validity(registration_code): """ This function checks if the registration code is valid, and then checks if it was already redeemed. """ diff --git a/shoppingcart/views.py b/shoppingcart/views.py index b065016..9936f46 100644 --- a/shoppingcart/views.py +++ b/shoppingcart/views.py @@ -127,6 +127,7 @@ def post(self, request): errors = {"user_message": "Wrong parameters on user creation"} return Response(errors, status=400) + # NOTE: there is a trailing space in the key in data; preserving in case clients rely on this behaviour. response = Response({"user_id ": user_id}, status=200) return response @@ -467,10 +468,7 @@ def post(self, request): user_is_valid = False error_reason = "User not found" try: - reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity( - enrollment_code, - request, - ) + reg_code_is_valid, reg_code_already_redeemed, course_registration = get_reg_code_validity(enrollment_code) except Http404: # only count toward the rate limiting if it was an invalid code is_ratelimited(request, key="user", group="enrollment-codes.enroll-user", rate="6/m", increment=True) diff --git a/test_settings.py b/test_settings.py deleted file mode 100644 index 1199351..0000000 --- a/test_settings.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -These settings are here to use during tests, because django requires them. - -In a real-world use case, apps in this project are installed into other -Django applications, so these settings will not be used. -""" - -from os.path import abspath, dirname, join - - -def root(*args): - """ - Get the absolute path of the given path relative to the project root. - """ - return join(abspath(dirname(__file__)), *args) - - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": "default.db", - "USER": "", - "PASSWORD": "", - "HOST": "", - "PORT": "", - } -} - -INSTALLED_APPS = ( - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.messages", - "django.contrib.sessions", - "shoppingcart", -) - -LOCALE_PATHS = [ - root("shoppingcart", "conf", "locale"), -] - -ROOT_URLCONF = "shoppingcart.urls" - -SECRET_KEY = "insecure-secret-key" - -MIDDLEWARE = ( - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", -) - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "APP_DIRS": False, - "OPTIONS": { - "context_processors": [ - "django.contrib.auth.context_processors.auth", # this is required for admin - "django.contrib.messages.context_processors.messages", # this is required for admin - ], - }, - } -] diff --git a/test_utils/__init__.py b/test_utils/__init__.py deleted file mode 100644 index 7961e47..0000000 --- a/test_utils/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Test utilities. - -Since pytest discourages putting __init__.py into testdirectory -(i.e. making tests a package) one cannot import from anywhere -under tests folder. However, some utility classes/methods might be useful -in multiple test modules (i.e. factoryboy factories, base test classes). - -So this package is the place to put them. -""" diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 623f6a5..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python -""" -Tests for the `legacy-appsembler-api` models module. -""" diff --git a/tox.ini b/tox.ini index 3c6f4a5..8bfc8f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,55 +1,17 @@ [tox] -envlist = quality,pii_check,migrations +envlist = format,quality,pii_check -[doc8] -; D001 = Line too long -ignore = D001 - -[pycodestyle] -exclude = .git,.tox,migrations -max-line-length = 100 - -[pydocstyle] -; D101 = Missing docstring in public class -; D200 = One-line docstring should fit on one line with quotes -; D203 = 1 blank line required before class docstring -; D212 = Multi-line docstring summary should start at the first line -; D215 = Section underline is over-indented (numpy style) -; D404 = First word of the docstring should not be This (numpy style) -; D405 = Section name should be properly capitalized (numpy style) -; D406 = Section name should end with a newline (numpy style) -; D407 = Missing dashed underline after section (numpy style) -; D408 = Section underline should be in the line following the section's name (numpy style) -; D409 = Section underline should match the length of its name (numpy style) -; D410 = Missing blank line after section (numpy style) -; D411 = Missing blank line before section (numpy style) -; D412 = No blank lines allowed between a section header and its content (numpy style) -; D413 = Missing blank line after last section (numpy style) -; D414 = Section has no content (numpy style) -ignore = D101,D200,D203,D212,D215,D404,D405,D406,D407,D408,D409,D410,D411,D412,D413,D414 -match-dir = (?!migrations) - -[pytest] -addopts = --cov shoppingcart --cov-report term-missing --cov-report xml -norecursedirs = .* docs requirements site-packages -django_find_project = false - -[testenv:quality] +[testenv:format] deps = -r{toxinidir}/requirements/quality.txt commands = - pylint shoppingcart manage.py setup.py tests test_utils test_settings.py - isort --check shoppingcart setup.py manage.py tests test_utils test_settings.py - black --check shoppingcart setup.py manage.py tests test_utils test_settings.py + isort shoppingcart setup.py manage.py + black shoppingcart setup.py manage.py -[testenv:pii_check] -deps = - -r{toxinidir}/requirements/test.txt -commands = - code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage - -[testenv:migrations] +[testenv:quality] deps = - -r{toxinidir}/requirements/base.txt + -r{toxinidir}/requirements/quality.txt commands = - python ./manage.py makemigrations shoppingcart --check --dry-run --verbosity 3 + pylint shoppingcart manage.py setup.py + isort --check shoppingcart setup.py manage.py + black --check shoppingcart setup.py manage.py