././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/0000755000175100001770000000000014527662421015262 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/AUTHORS.rst0000644000175100001770000000231314527662407017144 0ustar00runnerdockerPrimary author: * `Ben Lopatin `_ Contributors: * `Sebastian Annies `_ * `Paul Backhouse `_ * `Phil McMahon `_ * `Aaron Krill `_ * `Mauricio de Abreu Antunes `_ * `Omer Katz `_ * `Andrew Velis `_ * `Tom Davis `_ * `Nicolas Noirbent `_ * `Samuel Bishop `_ * `Eric Amador `_ * `Jon Miller `_ * `Robert Christopher `_ * `Basil Shubin `_ * `Federico Capoano `_ * `Justin Mayer `_ * `Alan Zhu `_ * `Samuel Spencer `_ * `Seb Vetter `_ * `KimSia Sim `_ * `Dan Moore `_ If your name is missing as a contributor that's my oversight, let me know at ben@benlopatin.com ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/HISTORY.rst0000644000175100001770000001376014527662407017170 0ustar00runnerdocker.. :changelog: History ======= 2.3.1 ----- * Update changelog to include all 2.3.0 changes 2.3.0 ----- * Includes django-extensions as dependency * Remove `six` dependency * Start testing against Python 3.12 2.2.0 ----- * Remove support for Django < 3.2 * Remove support for Python 3.7 * Fix trove classifiers 2.1.0 ----- * Adds migrations to support Django 4.x * Removes support for Django < 3.2 * Updates tox testing for supported configurations of Django 3.2 - 4.1 with Python 3.7 - 3.10 * Early tox testing of Django 4.1 with Python 3.11 * Fixes GitHub Actions automated testing 2.0.2 ----- * Remove `default_app_config` for forward compatiblity with Django 4.1 2.0.1 ----- * Better compatibility with Django 3.2 2.0.0 ----- * Invitation model backend uses models to track invitations * Registration/inviation backends take an optional namespace argument on initialization. The use case is if you want to namespace the URLs * Can provide a dotted path to `invitation_backend` and `registration_backend` functions * Drops support for Python 2 and Django versions < 2.2 LTS * Migrate the codebase to an src/ layout * Now with more test coverage! 1.1.1 ----- * Fixes issue with default backend where users defined without first/last names might not be represented 1.1.0 ----- * Migrations and slug related fixup This is a small but significant change. A change introduced in version 1.0.0 due to incompatability with an unmaintained default dependency yielded migration issues for many users. This release switches *back* to a *fork* of the original dependency to revert the default changes. 1.0.0 ----- * Django 2 compatibility. At this point it seems reasonable to bump to version 1. 0.9.3 ----- * Create username value for user if username field exists (custom user models) * Replaced BaseBackend._send_email with BaseBackend.email_message. email_message() should return the message without actually doing the send. 0.9.2 ----- * Decouple concrete organizations.Organization model from the invitation/registration backends 0.9.1 ----- * Fixes missing migration. Migration was created due to non-schema changes in models 0.9.0 ----- * Add notification to users when added to an organization * New abstract models create separation between 'plain' base models and abstract models that include abstracted functionality previously included only in concrete models * Python 3.6 and Django 1.11 test support 0.8.2 ----- * Updates setup classifiers information 0.8.1 ----- * Fixes [lack of] validation bug in backend registration form 0.8.0 ----- * Adds Django 1.10 support 0.7.0 ----- Fixes some issues which may require some users to clear out extraneous migrations produced by using configurable base classes. * Fixes condition where `create_organization` produces an owner who is not an admin user. * Fixes issue in slug field import resulting in spurious migrations. * Immediately deprecates configurable TimeStampedModel import. This caused serious problems with Django's migration library which were not easily resolved for a feature that added little value. 0.6.1 ----- * Fixes email parsing from settings 0.6.0 ----- * Adds Django 1.9 support * Drops support for Django 1.7 * Fixes migration issue related to incomplete support for configurable model fields and base model. If you are upgrading (especially from a fork of the development version of django-organization) you may have an extra migration, 0002_auto_20151005_1823, which has been removed. 0.5.3 ----- * Fixes migrations problem in build 0.5.2 ----- * Fixes packaging bug 0.5.1 ----- * Cleaned up installation instructions 0.5.0 ----- * Drops testing support for Django 1.5 and Django 1.6 * Adds native Django database migrations * Adds tested support for Django 1.7 and Django 1.8 0.4.3 ----- * Adds app specific signals 0.4.2 ----- * Various related name fixes in models, registration backends 0.4.1 ----- * Support for older Django versions with outdated versions of `six` 0.4.0 ----- * Allows for configurable TimeStampModel (base mixin for default Organization model) and AutoSlugField (field on default Organization model). 0.3.0 ----- * Initial Django 1.7 compatability release 0.2.3 ----- * Fix issue validating organziation ownership for custom organization models inheriting directly from the `Organization` class. 0.2.2 ----- * Packaging fix 0.2.1 ----- * Packaging fix 0.2.0 ----- * Abstract base models. These allow for custom organization models without relying on mulit-table inheritence, as well as custom organization user models, all on an app-by-app basis. 0.1.10 ------ * Packaging fix 0.1.9 ----- * Restructures tests to remove from installed module, should reduce installed package size 0.1.8 ----- * Fixes *another* bug in email invitations 0.1.7 ----- * Fixes bug in email invitation 0.1.6 ----- * Extends organizaton name length * Increase email field max length * Adds `get_or_add_user` method to Organization * Email character escaping 0.1.5 ----- * Use raw ID fields in admin * Fixes template variable names * Allow superusers access to all organization views * Activate related organizations when activating an owner user 0.1.4a ------ * Bug fix for user model import 0.1.4 ----- * Bugfixes for deleting organization users * Removes additional `auth.User` references in app code 0.1.3b ------ * Changes SlugField to an AutoSlugField from django-extensions * Base models on TimeStampedModel from django-extensions * ForeignKey to user model based on configurable user selection 0.1.3 ----- * Manage organization models with South * Added configurable context variable names to view mixins * Added a base backend class which the Invitation and Registration backends extend * Lengthed Organization name and slug fields * Makes mixin model classes configurable * Improved admin display * Removes initial passwords 0.1.2 ----- * Added registration backend * Various bug fixes 0.1.1 ----- * Add RequestContext to default invitation registration view * Fix invitations 0.1.0 ----- * Initial alpha application ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/LICENSE0000644000175100001770000000243214527662407016274 0ustar00runnerdockerCopyright (c) 2012-2019, Ben Lopatin and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/MANIFEST.in0000644000175100001770000000023614527662407017025 0ustar00runnerdockerinclude setup.py include setup.cfg include README.rst include MANIFEST.in include HISTORY.rst include LICENSE recursive-include src/organizations/templates * ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/PKG-INFO0000644000175100001770000002555214527662421016370 0ustar00runnerdockerMetadata-Version: 2.1 Name: django-organizations Version: 2.3.1 Summary: Group accounts for Django Home-page: https://github.com/bennylope/django-organizations/ Author: Ben Lopatin Author-email: ben@benlopatin.com License: BSD License Platform: OS Independent Classifier: Framework :: Django Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Development Status :: 5 - Production/Stable Requires-Python: >=3.8 License-File: LICENSE License-File: AUTHORS.rst Requires-Dist: Django>=3.2.0 Requires-Dist: django-extensions>=2.0.8 ==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |version| |wheel| |supported-versions| |supported-implementations| .. |docs| image:: https://readthedocs.org/projects/django-organizations/badge/?style=flat :target: https://readthedocs.org/projects/django-organizations :alt: Documentation Status .. |version| image:: https://img.shields.io/pypi/v/django-organizations.svg?style=flat :alt: PyPI Package latest release :target: https://pypi.python.org/pypi/django-organizations .. |wheel| image:: https://img.shields.io/pypi/wheel/django-organizations.svg?style=flat :alt: PyPI Wheel :target: https://pypi.python.org/pypi/django-organizations .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/django-organizations.svg?style=flat :alt: Supported versions :target: https://pypi.python.org/pypi/django-organizations .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/django-organizations.svg?style=flat :alt: Supported implementations :target: https://pypi.python.org/pypi/django-organizations .. end-table Separate individual user identity from accounts and subscriptions. Django Organizations adds user-managed, multi-user groups to your Django project. Use Django Organizations whether your site needs organizations that function like social groups or multi-user account objects to provide account and subscription functionality beyond the individual user. * Works with your existing user model, whether `django.contrib.auth` or a custom model. No additional user or authentication functionality required. * Users can be belong to and own more than one organization (account, group) * Invitation and registration functionality works out of the box for many situations and can be extended as need to fit specific requirements. * Start with the base models or use your own for greater customization. Documentation is on `Read the Docs `_ Development & Contributing ========================== **The master branch represents version 2 development. For updates related to 1.x versions of django-organizations pull requests should be made against the [`version-1.x` branch](tree/version-1.x).** Development is on-going. To-do items have been moved to the wiki for the time being. The basic functionality should not need much extending. Current dev priorities for me and contributors should include: * Improving the tests and test coverage (ideally moving them back out of the main module and executable using the setup.py file) * Improving the backends and backends concept so that additional invitation and registration backends can be used * Documentation * Ensuring all application text is translatable Please use the project's issues tracker to report bugs, doc updates, or other requests/suggestions. Targets & testing ----------------- The codebase is targeted and tested against: * Django 3.2.x against Python 3.8, 3.9, 3.10 * Django 4.1.x against Python 3.8, 3.9, 3.10, 3.11 * Django 4.2.x against Python 3.8, 3.9, 3.10, 3.11 To run the tests against all target environments, install `tox `_ and then execute the command:: tox Fast testing ------------ Testing each change on all the environments takes some time, you may want to test faster and avoid slowing down development by using pytest against your current environment:: pip install -r requirements-test.txt py.test Supply the ``-x`` option for **failfast** mode:: py.test -x Submitting ---------- These submission guidelines will make it more likely your submissions will be reviewed and make it into the project: * Ensure they match the project goals and are sufficiently generalized * Please try to follow `Django coding style `_. The code base style isn't all up to par, but I'd like it to move in that direction * Also please try to include `good commit log messages `_. * Pull requests should include an amount of code and commits that are reasonable to review, are **logically grouped**, and based off clean feature branches. Code contributions are expected to pass in all target environments, and pull requests should be made from branches with passing builds on `GitHub Actions `_. Project goals ------------- django-organizations should be backend agnostic: 1. Authentication agnostic 2. Registration agnostic 3. Invitation agnostic 4. User messaging agnostic Etc. Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations Check the `Release History tab `_ on the PyPI package page for pre-release versions. These can be downloaded by specifying the version. You can also install by downloading the source and running:: $ python setup.py install By default you will need to install `django-extensions` or comparable libraries if you plan on adding Django Organizations as an installed app to your Django project. See below on configuring. Configuring ----------- Make sure you have `django.contrib.auth` installed, and add the `organizations` application to your `INSTALLED_APPS` list: .. code-block:: python INSTALLED_APPS = ( ... 'django.contrib.auth', 'organizations', ) Then ensure that your project URL conf is updated. You should hook in the main application URL conf as well as your chosen invitation backend URLs: .. code-block:: python from organizations.backends import invitation_backend urlpatterns = [ ... url(r'^accounts/', include('organizations.urls')), url(r'^invitations/', include(invitation_backend().get_urls())), ] Auto slug field ~~~~~~~~~~~~~~~ The standard way of using Django Organizations is to use it as an installed app in your Django project. Django Organizations will need to use an auto slug field which are not included. By default it will try to import these from django-extensions, but you can configure your own in settings. The default: .. code-block:: python ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative: .. code-block:: python ORGS_SLUGFIELD = 'autoslug.fields.AutoSlugField' Previous versions allowed you to specify an `ORGS_TIMESTAMPED_MODEL` path. This is now ignored and the functionality satisfied by a vendored solution. A warning will be given but this *should not* have any effect on your code. - `django-extensions `_ - `Django AutoSlug `_ - `django-slugger `_ Registration & invitation backends ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can specify a different invitation backend in your project settings, and the `invitation_backend` function will provide the URLs defined by that backend: .. code-block:: python INVITATION_BACKEND = 'myapp.backends.MyInvitationBackend' Usage Overview ============== For most use cases it should be sufficient to include the app views directly using the default URL conf file. You can customize their functionality or access controls by extending the base views. There are three models: * **Organization** The group object. This is what you would associate your own app's functionality with, e.g. subscriptions, repositories, projects, etc. * **OrganizationUser** A custom `through` model for the ManyToMany relationship between the `Organization` model and the `User` model. It stores additional information about the user specific to the organization and provides a convenient link for organization ownership. * **OrganizationOwner** The user with rights over the life and death of the organization. This is a one to one relationship with the `OrganizationUser` model. This allows `User` objects to own multiple organizations and makes it easy to enforce ownership from within the organization's membership. The underlying organizations API is simple: .. code-block:: python >>> from organizations.utils import create_organization >>> chris = User.objects.get(username="chris") >>> soundgarden = create_organization(chris, "Soundgarden", org_user_defaults={'is_admin': True}) >>> soundgarden.is_member(chris) True >>> soundgarden.is_admin(chris) True >>> soundgarden.owner.organization_user >>> soundgarden.owner.organization_user.user >>> >>> audioslave = create_organization(chris, "Audioslave") >>> tom = User.objects.get(username="tom") >>> audioslave.add_user(tom, is_admin=True) Custom models ------------- Django-organizations can act as a base library (not installed in your project) and used to create unique organization model sets using custom tables. See the `Cooking with Django Organizations `_ section in the documentation for advice on proceeding. License ======= Anyone is free to use or modify this software under the terms of the BSD license. Sponsors ======== `Muster `_ is building precision advocacy software to impact policy through grassroots action. .. image:: https://www.muster.com/hs-fs/hubfs/muster_logo-2.png?width=600&name=muster_logo-2.png :target: https://www.muster.com/home?utm_source=github&campaign=opensource :width: 400 :alt: Alternative text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/README.rst0000644000175100001770000002361314527662407016762 0ustar00runnerdocker==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |version| |wheel| |supported-versions| |supported-implementations| .. |docs| image:: https://readthedocs.org/projects/django-organizations/badge/?style=flat :target: https://readthedocs.org/projects/django-organizations :alt: Documentation Status .. |version| image:: https://img.shields.io/pypi/v/django-organizations.svg?style=flat :alt: PyPI Package latest release :target: https://pypi.python.org/pypi/django-organizations .. |wheel| image:: https://img.shields.io/pypi/wheel/django-organizations.svg?style=flat :alt: PyPI Wheel :target: https://pypi.python.org/pypi/django-organizations .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/django-organizations.svg?style=flat :alt: Supported versions :target: https://pypi.python.org/pypi/django-organizations .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/django-organizations.svg?style=flat :alt: Supported implementations :target: https://pypi.python.org/pypi/django-organizations .. end-table Separate individual user identity from accounts and subscriptions. Django Organizations adds user-managed, multi-user groups to your Django project. Use Django Organizations whether your site needs organizations that function like social groups or multi-user account objects to provide account and subscription functionality beyond the individual user. * Works with your existing user model, whether `django.contrib.auth` or a custom model. No additional user or authentication functionality required. * Users can be belong to and own more than one organization (account, group) * Invitation and registration functionality works out of the box for many situations and can be extended as need to fit specific requirements. * Start with the base models or use your own for greater customization. Documentation is on `Read the Docs `_ Development & Contributing ========================== **The master branch represents version 2 development. For updates related to 1.x versions of django-organizations pull requests should be made against the [`version-1.x` branch](tree/version-1.x).** Development is on-going. To-do items have been moved to the wiki for the time being. The basic functionality should not need much extending. Current dev priorities for me and contributors should include: * Improving the tests and test coverage (ideally moving them back out of the main module and executable using the setup.py file) * Improving the backends and backends concept so that additional invitation and registration backends can be used * Documentation * Ensuring all application text is translatable Please use the project's issues tracker to report bugs, doc updates, or other requests/suggestions. Targets & testing ----------------- The codebase is targeted and tested against: * Django 3.2.x against Python 3.8, 3.9, 3.10 * Django 4.1.x against Python 3.8, 3.9, 3.10, 3.11 * Django 4.2.x against Python 3.8, 3.9, 3.10, 3.11 To run the tests against all target environments, install `tox `_ and then execute the command:: tox Fast testing ------------ Testing each change on all the environments takes some time, you may want to test faster and avoid slowing down development by using pytest against your current environment:: pip install -r requirements-test.txt py.test Supply the ``-x`` option for **failfast** mode:: py.test -x Submitting ---------- These submission guidelines will make it more likely your submissions will be reviewed and make it into the project: * Ensure they match the project goals and are sufficiently generalized * Please try to follow `Django coding style `_. The code base style isn't all up to par, but I'd like it to move in that direction * Also please try to include `good commit log messages `_. * Pull requests should include an amount of code and commits that are reasonable to review, are **logically grouped**, and based off clean feature branches. Code contributions are expected to pass in all target environments, and pull requests should be made from branches with passing builds on `GitHub Actions `_. Project goals ------------- django-organizations should be backend agnostic: 1. Authentication agnostic 2. Registration agnostic 3. Invitation agnostic 4. User messaging agnostic Etc. Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations Check the `Release History tab `_ on the PyPI package page for pre-release versions. These can be downloaded by specifying the version. You can also install by downloading the source and running:: $ python setup.py install By default you will need to install `django-extensions` or comparable libraries if you plan on adding Django Organizations as an installed app to your Django project. See below on configuring. Configuring ----------- Make sure you have `django.contrib.auth` installed, and add the `organizations` application to your `INSTALLED_APPS` list: .. code-block:: python INSTALLED_APPS = ( ... 'django.contrib.auth', 'organizations', ) Then ensure that your project URL conf is updated. You should hook in the main application URL conf as well as your chosen invitation backend URLs: .. code-block:: python from organizations.backends import invitation_backend urlpatterns = [ ... url(r'^accounts/', include('organizations.urls')), url(r'^invitations/', include(invitation_backend().get_urls())), ] Auto slug field ~~~~~~~~~~~~~~~ The standard way of using Django Organizations is to use it as an installed app in your Django project. Django Organizations will need to use an auto slug field which are not included. By default it will try to import these from django-extensions, but you can configure your own in settings. The default: .. code-block:: python ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative: .. code-block:: python ORGS_SLUGFIELD = 'autoslug.fields.AutoSlugField' Previous versions allowed you to specify an `ORGS_TIMESTAMPED_MODEL` path. This is now ignored and the functionality satisfied by a vendored solution. A warning will be given but this *should not* have any effect on your code. - `django-extensions `_ - `Django AutoSlug `_ - `django-slugger `_ Registration & invitation backends ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can specify a different invitation backend in your project settings, and the `invitation_backend` function will provide the URLs defined by that backend: .. code-block:: python INVITATION_BACKEND = 'myapp.backends.MyInvitationBackend' Usage Overview ============== For most use cases it should be sufficient to include the app views directly using the default URL conf file. You can customize their functionality or access controls by extending the base views. There are three models: * **Organization** The group object. This is what you would associate your own app's functionality with, e.g. subscriptions, repositories, projects, etc. * **OrganizationUser** A custom `through` model for the ManyToMany relationship between the `Organization` model and the `User` model. It stores additional information about the user specific to the organization and provides a convenient link for organization ownership. * **OrganizationOwner** The user with rights over the life and death of the organization. This is a one to one relationship with the `OrganizationUser` model. This allows `User` objects to own multiple organizations and makes it easy to enforce ownership from within the organization's membership. The underlying organizations API is simple: .. code-block:: python >>> from organizations.utils import create_organization >>> chris = User.objects.get(username="chris") >>> soundgarden = create_organization(chris, "Soundgarden", org_user_defaults={'is_admin': True}) >>> soundgarden.is_member(chris) True >>> soundgarden.is_admin(chris) True >>> soundgarden.owner.organization_user >>> soundgarden.owner.organization_user.user >>> >>> audioslave = create_organization(chris, "Audioslave") >>> tom = User.objects.get(username="tom") >>> audioslave.add_user(tom, is_admin=True) Custom models ------------- Django-organizations can act as a base library (not installed in your project) and used to create unique organization model sets using custom tables. See the `Cooking with Django Organizations `_ section in the documentation for advice on proceeding. License ======= Anyone is free to use or modify this software under the terms of the BSD license. Sponsors ======== `Muster `_ is building precision advocacy software to impact policy through grassroots action. .. image:: https://www.muster.com/hs-fs/hubfs/muster_logo-2.png?width=600&name=muster_logo-2.png :target: https://www.muster.com/home?utm_source=github&campaign=opensource :width: 400 :alt: Alternative text ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/setup.cfg0000644000175100001770000000216014527662421017102 0ustar00runnerdocker[metadata] name = django-organizations version = attr: organizations.__version__ author = Ben Lopatin author_email = ben@benlopatin.com url = https://github.com/bennylope/django-organizations/ description = Group accounts for Django long_description = file: README.rst license = BSD License platforms = OS Independent classifiers = Framework :: Django Environment :: Web Environment Intended Audience :: Developers License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: CPython Development Status :: 5 - Production/Stable [options] zip_safe = False include_package_data = True packages = find: package_dir = =src install_requires = Django>=3.2.0 django-extensions>=2.0.8 python_requires = >=3.8 [options.packages.find] where = src [bdist_wheel] universal = 1 [build-system] requires = setuptools >= "40.9.0" wheel [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/setup.py0000644000175100001770000000012314527662407016774 0ustar00runnerdocker""" See setup.cfg for packaging settings """ from setuptools import setup setup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7061753 django-organizations-2.3.1/src/0000755000175100001770000000000014527662421016051 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/src/django_organizations.egg-info/0000755000175100001770000000000014527662421023754 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/PKG-INFO0000644000175100001770000002555214527662420025061 0ustar00runnerdockerMetadata-Version: 2.1 Name: django-organizations Version: 2.3.1 Summary: Group accounts for Django Home-page: https://github.com/bennylope/django-organizations/ Author: Ben Lopatin Author-email: ben@benlopatin.com License: BSD License Platform: OS Independent Classifier: Framework :: Django Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Development Status :: 5 - Production/Stable Requires-Python: >=3.8 License-File: LICENSE License-File: AUTHORS.rst Requires-Dist: Django>=3.2.0 Requires-Dist: django-extensions>=2.0.8 ==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |version| |wheel| |supported-versions| |supported-implementations| .. |docs| image:: https://readthedocs.org/projects/django-organizations/badge/?style=flat :target: https://readthedocs.org/projects/django-organizations :alt: Documentation Status .. |version| image:: https://img.shields.io/pypi/v/django-organizations.svg?style=flat :alt: PyPI Package latest release :target: https://pypi.python.org/pypi/django-organizations .. |wheel| image:: https://img.shields.io/pypi/wheel/django-organizations.svg?style=flat :alt: PyPI Wheel :target: https://pypi.python.org/pypi/django-organizations .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/django-organizations.svg?style=flat :alt: Supported versions :target: https://pypi.python.org/pypi/django-organizations .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/django-organizations.svg?style=flat :alt: Supported implementations :target: https://pypi.python.org/pypi/django-organizations .. end-table Separate individual user identity from accounts and subscriptions. Django Organizations adds user-managed, multi-user groups to your Django project. Use Django Organizations whether your site needs organizations that function like social groups or multi-user account objects to provide account and subscription functionality beyond the individual user. * Works with your existing user model, whether `django.contrib.auth` or a custom model. No additional user or authentication functionality required. * Users can be belong to and own more than one organization (account, group) * Invitation and registration functionality works out of the box for many situations and can be extended as need to fit specific requirements. * Start with the base models or use your own for greater customization. Documentation is on `Read the Docs `_ Development & Contributing ========================== **The master branch represents version 2 development. For updates related to 1.x versions of django-organizations pull requests should be made against the [`version-1.x` branch](tree/version-1.x).** Development is on-going. To-do items have been moved to the wiki for the time being. The basic functionality should not need much extending. Current dev priorities for me and contributors should include: * Improving the tests and test coverage (ideally moving them back out of the main module and executable using the setup.py file) * Improving the backends and backends concept so that additional invitation and registration backends can be used * Documentation * Ensuring all application text is translatable Please use the project's issues tracker to report bugs, doc updates, or other requests/suggestions. Targets & testing ----------------- The codebase is targeted and tested against: * Django 3.2.x against Python 3.8, 3.9, 3.10 * Django 4.1.x against Python 3.8, 3.9, 3.10, 3.11 * Django 4.2.x against Python 3.8, 3.9, 3.10, 3.11 To run the tests against all target environments, install `tox `_ and then execute the command:: tox Fast testing ------------ Testing each change on all the environments takes some time, you may want to test faster and avoid slowing down development by using pytest against your current environment:: pip install -r requirements-test.txt py.test Supply the ``-x`` option for **failfast** mode:: py.test -x Submitting ---------- These submission guidelines will make it more likely your submissions will be reviewed and make it into the project: * Ensure they match the project goals and are sufficiently generalized * Please try to follow `Django coding style `_. The code base style isn't all up to par, but I'd like it to move in that direction * Also please try to include `good commit log messages `_. * Pull requests should include an amount of code and commits that are reasonable to review, are **logically grouped**, and based off clean feature branches. Code contributions are expected to pass in all target environments, and pull requests should be made from branches with passing builds on `GitHub Actions `_. Project goals ------------- django-organizations should be backend agnostic: 1. Authentication agnostic 2. Registration agnostic 3. Invitation agnostic 4. User messaging agnostic Etc. Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations Check the `Release History tab `_ on the PyPI package page for pre-release versions. These can be downloaded by specifying the version. You can also install by downloading the source and running:: $ python setup.py install By default you will need to install `django-extensions` or comparable libraries if you plan on adding Django Organizations as an installed app to your Django project. See below on configuring. Configuring ----------- Make sure you have `django.contrib.auth` installed, and add the `organizations` application to your `INSTALLED_APPS` list: .. code-block:: python INSTALLED_APPS = ( ... 'django.contrib.auth', 'organizations', ) Then ensure that your project URL conf is updated. You should hook in the main application URL conf as well as your chosen invitation backend URLs: .. code-block:: python from organizations.backends import invitation_backend urlpatterns = [ ... url(r'^accounts/', include('organizations.urls')), url(r'^invitations/', include(invitation_backend().get_urls())), ] Auto slug field ~~~~~~~~~~~~~~~ The standard way of using Django Organizations is to use it as an installed app in your Django project. Django Organizations will need to use an auto slug field which are not included. By default it will try to import these from django-extensions, but you can configure your own in settings. The default: .. code-block:: python ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative: .. code-block:: python ORGS_SLUGFIELD = 'autoslug.fields.AutoSlugField' Previous versions allowed you to specify an `ORGS_TIMESTAMPED_MODEL` path. This is now ignored and the functionality satisfied by a vendored solution. A warning will be given but this *should not* have any effect on your code. - `django-extensions `_ - `Django AutoSlug `_ - `django-slugger `_ Registration & invitation backends ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can specify a different invitation backend in your project settings, and the `invitation_backend` function will provide the URLs defined by that backend: .. code-block:: python INVITATION_BACKEND = 'myapp.backends.MyInvitationBackend' Usage Overview ============== For most use cases it should be sufficient to include the app views directly using the default URL conf file. You can customize their functionality or access controls by extending the base views. There are three models: * **Organization** The group object. This is what you would associate your own app's functionality with, e.g. subscriptions, repositories, projects, etc. * **OrganizationUser** A custom `through` model for the ManyToMany relationship between the `Organization` model and the `User` model. It stores additional information about the user specific to the organization and provides a convenient link for organization ownership. * **OrganizationOwner** The user with rights over the life and death of the organization. This is a one to one relationship with the `OrganizationUser` model. This allows `User` objects to own multiple organizations and makes it easy to enforce ownership from within the organization's membership. The underlying organizations API is simple: .. code-block:: python >>> from organizations.utils import create_organization >>> chris = User.objects.get(username="chris") >>> soundgarden = create_organization(chris, "Soundgarden", org_user_defaults={'is_admin': True}) >>> soundgarden.is_member(chris) True >>> soundgarden.is_admin(chris) True >>> soundgarden.owner.organization_user >>> soundgarden.owner.organization_user.user >>> >>> audioslave = create_organization(chris, "Audioslave") >>> tom = User.objects.get(username="tom") >>> audioslave.add_user(tom, is_admin=True) Custom models ------------- Django-organizations can act as a base library (not installed in your project) and used to create unique organization model sets using custom tables. See the `Cooking with Django Organizations `_ section in the documentation for advice on proceeding. License ======= Anyone is free to use or modify this software under the terms of the BSD license. Sponsors ======== `Muster `_ is building precision advocacy software to impact policy through grassroots action. .. image:: https://www.muster.com/hs-fs/hubfs/muster_logo-2.png?width=600&name=muster_logo-2.png :target: https://www.muster.com/home?utm_source=github&campaign=opensource :width: 400 :alt: Alternative text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/SOURCES.txt0000644000175100001770000000660114527662420025642 0ustar00runnerdockerAUTHORS.rst HISTORY.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py src/django_organizations.egg-info/PKG-INFO src/django_organizations.egg-info/SOURCES.txt src/django_organizations.egg-info/dependency_links.txt src/django_organizations.egg-info/not-zip-safe src/django_organizations.egg-info/requires.txt src/django_organizations.egg-info/top_level.txt src/organizations/__init__.py src/organizations/abstract.py src/organizations/admin.py src/organizations/app_settings.py src/organizations/apps.py src/organizations/base.py src/organizations/base_admin.py src/organizations/exceptions.py src/organizations/fields.py src/organizations/forms.py src/organizations/managers.py src/organizations/models.py src/organizations/signals.py src/organizations/urls.py src/organizations/utils.py src/organizations/backends/__init__.py src/organizations/backends/defaults.py src/organizations/backends/forms.py src/organizations/backends/modeled.py src/organizations/migrations/0001_initial.py src/organizations/migrations/0002_model_update.py src/organizations/migrations/0003_field_fix_and_editable.py src/organizations/migrations/0004_organizationinvitation.py src/organizations/migrations/0005_alter_organization_users_and_more.py src/organizations/migrations/0006_alter_organization_slug.py src/organizations/migrations/__init__.py src/organizations/templates/organizations_base.html src/organizations/templates/organizations/invitation_join.html src/organizations/templates/organizations/login.html src/organizations/templates/organizations/organization_confirm_delete.html src/organizations/templates/organizations/organization_detail.html src/organizations/templates/organizations/organization_form.html src/organizations/templates/organizations/organization_list.html src/organizations/templates/organizations/organization_users.html src/organizations/templates/organizations/organizationuser_confirm_delete.html src/organizations/templates/organizations/organizationuser_detail.html src/organizations/templates/organizations/organizationuser_form.html src/organizations/templates/organizations/organizationuser_list.html src/organizations/templates/organizations/organizationuser_remind.html src/organizations/templates/organizations/register_form.html src/organizations/templates/organizations/register_success.html src/organizations/templates/organizations/email/activation_body.html src/organizations/templates/organizations/email/activation_subject.txt src/organizations/templates/organizations/email/invitation_body.html src/organizations/templates/organizations/email/invitation_subject.txt src/organizations/templates/organizations/email/modeled_invitation_body.html src/organizations/templates/organizations/email/modeled_invitation_subject.txt src/organizations/templates/organizations/email/notification_body.html src/organizations/templates/organizations/email/notification_subject.txt src/organizations/templates/organizations/email/reminder_body.html src/organizations/templates/organizations/email/reminder_subject.txt src/organizations/templatetags/__init__.py src/organizations/templatetags/org_tags.py src/organizations/views/__init__.py src/organizations/views/base.py src/organizations/views/default.py src/organizations/views/mixins.py tests/test_fields.py tests/test_forms.py tests/test_migrations.py tests/test_mixins.py tests/test_models.py tests/test_signals.py tests/test_templatetags.py tests/test_utils.py tests/test_views.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/dependency_links.txt0000644000175100001770000000000114527662420030021 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/not-zip-safe0000644000175100001770000000000114527662420026201 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/requires.txt0000644000175100001770000000004714527662420026354 0ustar00runnerdockerDjango>=3.2.0 django-extensions>=2.0.8 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750608.0 django-organizations-2.3.1/src/django_organizations.egg-info/top_level.txt0000644000175100001770000000001614527662420026502 0ustar00runnerdockerorganizations ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7141752 django-organizations-2.3.1/src/organizations/0000755000175100001770000000000014527662421020740 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/__init__.py0000644000175100001770000000015114527662407023052 0ustar00runnerdocker#!/usr/bin/env python __author__ = "Ben Lopatin" __email__ = "ben@benlopatin.com" __version__ = "2.3.1" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/abstract.py0000644000175100001770000002044114527662407023122 0ustar00runnerdocker import warnings from django.conf import settings from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ from organizations.base import AbstractBaseInvitation from organizations.base import AbstractBaseOrganization from organizations.base import AbstractBaseOrganizationOwner from organizations.base import AbstractBaseOrganizationUser from organizations.base import with_metaclass from organizations.base import OrgMeta from organizations.fields import AutoCreatedField from organizations.fields import AutoLastModifiedField from organizations.fields import SlugField from organizations.signals import owner_changed from organizations.signals import user_added from organizations.signals import user_removed USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User") ORGS_TIMESTAMPED_MODEL = getattr(settings, "ORGS_TIMESTAMPED_MODEL", None) if ORGS_TIMESTAMPED_MODEL: warnings.warn( "Configured TimestampModel has been replaced and is now ignored.", DeprecationWarning, ) class SharedBaseModel(models.Model): """ Adds fields ``created`` and ``modified`` and two private methods that are used by the rest of the abstract models. """ created = AutoCreatedField() modified = AutoLastModifiedField() @property def _org_user_model(self): model = self.__class__.module_registry[self.__class__.__module__][ "OrgUserModel" ] if model is None: model = self.__class__.module_registry["organizations.models"][ "OrgUserModel" ] return model @property def _org_owner_model(self): model = self.__class__.module_registry[self.__class__.__module__][ "OrgOwnerModel" ] if model is None: model = self.__class__.module_registry["organizations.models"][ "OrgOwnerModel" ] return model class Meta: abstract = True class AbstractOrganization( with_metaclass(OrgMeta, SharedBaseModel, AbstractBaseOrganization) ): """ Abstract Organization model. """ slug = SlugField( max_length=200, blank=False, editable=True, populate_from="name", unique=True, help_text=_("The name in all lowercase, suitable for URL identification"), ) class Meta(AbstractBaseOrganization.Meta): abstract = True verbose_name = _("organization") verbose_name_plural = _("organizations") def __str__(self): return self.name def get_absolute_url(self): return reverse("organization_detail", kwargs={"organization_pk": self.pk}) def add_user(self, user, is_admin=False): """ Adds a new user and if the first user makes the user an admin and the owner. """ users_count = self.users.all().count() if users_count == 0: is_admin = True # TODO get specific org user? org_user = self._org_user_model.objects.create( user=user, organization=self, is_admin=is_admin ) if users_count == 0: # TODO get specific org user? self._org_owner_model.objects.create( organization=self, organization_user=org_user ) # User added signal user_added.send(sender=self, user=user) return org_user def remove_user(self, user): """ Deletes a user from an organization. """ org_user = self._org_user_model.objects.get(user=user, organization=self) org_user.delete() # User removed signal user_removed.send(sender=self, user=user) def get_or_add_user(self, user, **kwargs): """ Adds a new user to the organization, and if it's the first user makes the user an admin and the owner. Uses the `get_or_create` method to create or return the existing user. `user` should be a user instance, e.g. `auth.User`. Returns the same tuple as the `get_or_create` method, the `OrganizationUser` and a boolean value indicating whether the OrganizationUser was created or not. """ is_admin = kwargs.pop("is_admin", False) users_count = self.users.all().count() if users_count == 0: is_admin = True org_user, created = self._org_user_model.objects.get_or_create( organization=self, user=user, defaults={"is_admin": is_admin} ) if users_count == 0: self._org_owner_model.objects.create( organization=self, organization_user=org_user ) if created: # User added signal user_added.send(sender=self, user=user) return org_user, created def change_owner(self, new_owner): """ Changes ownership of an organization. """ old_owner = self.owner.organization_user self.owner.organization_user = new_owner self.owner.save() # Owner changed signal owner_changed.send(sender=self, old=old_owner, new=new_owner) def is_admin(self, user): """ Returns True is user is an admin in the organization, otherwise false """ return ( True if self.organization_users.filter(user=user, is_admin=True) else False ) def is_owner(self, user): """ Returns True is user is the organization's owner, otherwise false """ return self.owner.organization_user.user == user class AbstractOrganizationUser( with_metaclass(OrgMeta, SharedBaseModel, AbstractBaseOrganizationUser) ): """ Abstract OrganizationUser model """ is_admin = models.BooleanField(default=False) class Meta(AbstractBaseOrganizationUser.Meta): abstract = True verbose_name = _("organization user") verbose_name_plural = _("organization users") def __str__(self): return "{0} ({1})".format( self.name if self.user.is_active else self.user.email, self.organization.name, ) def delete(self, using=None): """ If the organization user is also the owner, this should not be deleted unless it's part of a cascade from the Organization. If there is no owner then the deletion should proceed. """ from organizations.exceptions import OwnershipRequired try: if self.organization.owner.organization_user.pk == self.pk: raise OwnershipRequired( _( "Cannot delete organization owner " "before organization or transferring ownership." ) ) # TODO This line presumes that OrgOwner model can't be modified except self._org_owner_model.DoesNotExist: pass super().delete(using=using) def get_absolute_url(self): return reverse( "organization_user_detail", kwargs={"organization_pk": self.organization.pk, "user_pk": self.user.pk}, ) class AbstractOrganizationOwner( with_metaclass(OrgMeta, SharedBaseModel, AbstractBaseOrganizationOwner) ): """ Abstract OrganizationOwner model """ class Meta: abstract = True verbose_name = _("organization owner") verbose_name_plural = _("organization owners") def save(self, *args, **kwargs): """ Extends the default save method by verifying that the chosen organization user is associated with the organization. Method validates against the primary key of the organization because when validating an inherited model it may be checking an instance of `Organization` against an instance of `CustomOrganization`. Mutli-table inheritance means the database keys will be identical though. """ from organizations.exceptions import OrganizationMismatch if self.organization_user.organization.pk != self.organization.pk: raise OrganizationMismatch else: super().save(*args, **kwargs) class AbstractOrganizationInvitation( with_metaclass(OrgMeta, SharedBaseModel, AbstractBaseInvitation) ): """ Abstract OrganizationInvitationBase model """ class Meta: abstract = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/admin.py0000644000175100001770000000147314527662407022413 0ustar00runnerdocker from django.contrib import admin from organizations import models from organizations.base_admin import BaseOrganizationAdmin from organizations.base_admin import BaseOrganizationOwnerAdmin from organizations.base_admin import BaseOrganizationUserAdmin from organizations.base_admin import BaseOwnerInline class OwnerInline(BaseOwnerInline): model = models.OrganizationOwner @admin.register(models.Organization) class OrganizationAdmin(BaseOrganizationAdmin): inlines = [OwnerInline] @admin.register(models.OrganizationUser) class OrganizationUserAdmin(BaseOrganizationUserAdmin): pass @admin.register(models.OrganizationOwner) class OrganizationOwnerAdmin(BaseOrganizationOwnerAdmin): pass @admin.register(models.OrganizationInvitation) class OrganizationInvitationAdmin(admin.ModelAdmin): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/app_settings.py0000644000175100001770000000071614527662407024022 0ustar00runnerdocker from django.conf import settings from django.contrib.auth.models import User from organizations.utils import model_field_attr ORGS_INVITATION_BACKEND = getattr( settings, "INVITATION_BACKEND", "organizations.backends.defaults.InvitationBackend" ) ORGS_REGISTRATION_BACKEND = getattr( settings, "REGISTRATION_BACKEND", "organizations.backends.defaults.RegistrationBackend", ) ORGS_EMAIL_LENGTH = model_field_attr(User, "email", "max_length") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/apps.py0000644000175100001770000000027714527662407022267 0ustar00runnerdocker from django.apps import AppConfig class OrganizationsConfig(AppConfig): name = "organizations" verbose_name = "Organizations" default_auto_field = 'django.db.models.AutoField' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7141752 django-organizations-2.3.1/src/organizations/backends/0000755000175100001770000000000014527662421022512 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/backends/__init__.py0000644000175100001770000000256414527662407024636 0ustar00runnerdocker from importlib import import_module from typing import Optional # noqa from typing import Text # noqa from organizations.app_settings import ORGS_INVITATION_BACKEND from organizations.app_settings import ORGS_REGISTRATION_BACKEND from organizations.backends.defaults import BaseBackend # noqa def invitation_backend(backend=None, namespace=None): # type: (Optional[Text], Optional[Text]) -> BaseBackend """ Returns a specified invitation backend Args: backend: dotted path to the invitation backend class namespace: URL namespace to use Returns: an instance of an InvitationBackend """ backend = backend or ORGS_INVITATION_BACKEND class_module, class_name = backend.rsplit(".", 1) mod = import_module(class_module) return getattr(mod, class_name)(namespace=namespace) def registration_backend(backend=None, namespace=None): # type: (Optional[Text], Optional[Text]) -> BaseBackend """ Returns a specified registration backend Args: backend: dotted path to the registration backend class namespace: URL namespace to use Returns: an instance of an RegistrationBackend """ backend = backend or ORGS_REGISTRATION_BACKEND class_module, class_name = backend.rsplit(".", 1) mod = import_module(class_module) return getattr(mod, class_name)(namespace=namespace) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/backends/defaults.py0000644000175100001770000003201114527662407024674 0ustar00runnerdocker """Backend classes should provide common interface""" import email.utils import inspect import uuid from typing import ClassVar # noqa from typing import Optional # noqa from typing import Text # noqa from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth import get_user_model from django.contrib.auth import login from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core.mail import EmailMessage from django.http import Http404 from django.shortcuts import redirect from django.shortcuts import render from django.template import loader from django.urls import path from django.urls import reverse from django.utils.translation import gettext as _ from organizations.backends.forms import UserRegistrationForm from organizations.backends.forms import org_registration_form from organizations.utils import create_organization from organizations.utils import default_org_model from organizations.utils import model_field_attr class BaseBackend: """ Base backend class for registering and inviting users to an organization """ registration_form_template = "organizations/register_form.html" activation_success_template = "organizations/register_success.html" def __init__(self, org_model=None, namespace=None): # type: (Optional[ClassVar], Optional[Text]) -> None self.user_model = get_user_model() self.org_model = org_model or default_org_model() self.namespace = namespace def namespace_preface(self): return "" if not self.namespace else "{}:".format(self.namespace) def get_urls(self): raise NotImplementedError def get_success_url(self): """Will return the class's `success_url` attribute unless overridden""" raise NotImplementedError def get_form(self, **kwargs): """Returns the form for registering or inviting a user""" if not getattr(self, "form_class", None): raise AttributeError("You must define a form_class") return self.form_class(**kwargs) def get_token(self, user, **kwargs): """Returns a unique token for the given user""" return PasswordResetTokenGenerator().make_token(user) def get_username(self): """ Returns a UUID-based 'random' and unique username. This is required data for user models with a username field. """ return str(uuid.uuid4())[ : model_field_attr(self.user_model, "username", "max_length") ] def activate_organizations(self, user): """ Activates the related organizations for the user. It only activates the related organizations by model type - that is, if there are multiple types of organizations then only organizations in the provided model class are activated. """ try: relation_name = self.org_model().user_relation_name except TypeError: # No org_model specified, raises a TypeError because NoneType is # not callable. This the most sensible default: relation_name = "organizations_organization" organization_set = getattr(user, relation_name) for org in organization_set.filter(is_active=False): org.is_active = True org.save() def activate_view(self, request, user_id, token): """ View function that activates the given User by setting `is_active` to true if the provided information is verified. """ try: user = self.user_model.objects.get(id=user_id, is_active=False) except self.user_model.DoesNotExist: raise Http404(_("Your URL may have expired.")) if not PasswordResetTokenGenerator().check_token(user, token): raise Http404(_("Your URL may have expired.")) form = self.get_form( data=request.POST or None, files=request.FILES or None, instance=user ) if form.is_valid(): form.instance.is_active = True user = form.save() user.set_password(form.cleaned_data["password1"]) user.save() self.activate_organizations(user) user = authenticate( username=form.cleaned_data["username"], password=form.cleaned_data["password1"], ) if user is None: raise Http404(_("Can't authenticate user")) login(request, user) return redirect(self.get_success_url()) return render(request, self.registration_form_template, {"form": form}) def send_reminder(self, user, sender=None, **kwargs): """Sends a reminder email to the specified user""" if user.is_active: return False token = PasswordResetTokenGenerator().make_token(user) kwargs.update({"token": token}) self.email_message( user, self.reminder_subject, self.reminder_body, sender, **kwargs ).send() def email_message( self, user, subject_template, body_template, sender=None, message_class=EmailMessage, **kwargs ): """ Returns an email message for a new user. This can be easily overridden. For instance, to send an HTML message, use the EmailMultiAlternatives message_class and attach the additional conent. """ if sender: try: display_name = sender.get_full_name() except (AttributeError, TypeError): display_name = sender.get_username() from_email = "%s <%s>" % ( display_name, email.utils.parseaddr(settings.DEFAULT_FROM_EMAIL)[1], ) reply_to = "%s <%s>" % (display_name, sender.email) else: from_email = settings.DEFAULT_FROM_EMAIL reply_to = from_email headers = {"Reply-To": reply_to} kwargs.update({"sender": sender, "user": user}) subject_template = loader.get_template(subject_template) body_template = loader.get_template(body_template) subject = subject_template.render( kwargs ).strip() # Remove stray newline characters body = body_template.render(kwargs) return message_class(subject, body, from_email, [user.email], headers=headers) class RegistrationBackend(BaseBackend): """ A backend for allowing new users to join the site by creating a new user associated with a new organization. """ # NOTE this backend stands to be simplified further, as email verification # should be beyond the purview of this app activation_subject = "organizations/email/activation_subject.txt" activation_body = "organizations/email/activation_body.html" reminder_subject = "organizations/email/reminder_subject.txt" reminder_body = "organizations/email/reminder_body.html" form_class = UserRegistrationForm def get_success_url(self): return reverse("registration_success") def get_urls(self): return [ path("complete/", view=self.success_view, name="registration_success"), path( "-/", view=self.activate_view, name="registration_register", ), path("", view=self.create_view, name="registration_create"), ] @property def urls(self): return self.get_urls(), self.namespace or "registration" def register_by_email(self, email, sender=None, request=None, **kwargs): """ Returns a User object filled with dummy data and not active, and sends an invitation email. """ try: user = self.user_model.objects.get(email=email) except self.user_model.DoesNotExist: user = self.user_model.objects.create( username=self.get_username(), email=email, password=self.user_model.objects.make_random_password(), ) user.is_active = False user.save() self.send_activation(user, sender, **kwargs) return user def send_activation(self, user, sender=None, **kwargs): """ Invites a user to join the site """ if user.is_active: return False token = self.get_token(user) kwargs.update({"token": token}) self.email_message( user, self.activation_subject, self.activation_body, sender, **kwargs ).send() def create_view(self, request): """ Initiates the organization and user account creation process """ if request.user.is_authenticated: return redirect("organization_add") form = org_registration_form(self.org_model)(request.POST or None) if form.is_valid(): try: user = self.user_model.objects.get(email=form.cleaned_data["email"]) except self.user_model.DoesNotExist: user = self.user_model.objects.create( username=self.get_username(), email=form.cleaned_data["email"], password=self.user_model.objects.make_random_password(), ) user.is_active = False user.save() else: return redirect("organization_add") organization = create_organization( user, form.cleaned_data["name"], form.cleaned_data["slug"], is_active=False, ) return render( request, self.activation_success_template, {"user": user, "organization": organization}, ) return render(request, self.registration_form_template, {"form": form}) def success_view(self, request): return render(request, self.activation_success_template, {}) class InvitationBackend(BaseBackend): """ A backend for inviting new users to join the site as members of an organization. """ notification_subject = "organizations/email/notification_subject.txt" notification_body = "organizations/email/notification_body.html" invitation_subject = "organizations/email/invitation_subject.txt" invitation_body = "organizations/email/invitation_body.html" reminder_subject = "organizations/email/reminder_subject.txt" reminder_body = "organizations/email/reminder_body.html" form_class = UserRegistrationForm def get_success_url(self): # TODO get this url name from an attribute return reverse("organization_list") def get_urls(self): # TODO enable naming based on a model? return [ path( "-/", view=self.activate_view, name="invitations_register", ) ] def invite_by_email(self, email, sender=None, request=None, **kwargs): """Creates an inactive user with the information we know and then sends an invitation email for that user to complete registration. If your project uses email in a different way then you should make to extend this method as it only checks the `email` attribute for Users. """ try: user = self.user_model.objects.get(email=email) except self.user_model.DoesNotExist: # TODO break out user creation process if ( "username" in inspect.getfullargspec(self.user_model.objects.create_user).args ): user = self.user_model.objects.create( username=self.get_username(), email=email, password=self.user_model.objects.make_random_password(), ) else: user = self.user_model.objects.create( email=email, password=self.user_model.objects.make_random_password() ) user.is_active = False user.save() self.send_invitation(user, sender, **kwargs) return user def send_invitation(self, user, sender=None, **kwargs): """An intermediary function for sending an invitation email that selects the templates, generating the token, and ensuring that the user has not already joined the site. """ if user.is_active: return False token = self.get_token(user) kwargs.update({"token": token}) self.email_message( user, self.invitation_subject, self.invitation_body, sender, **kwargs ).send() return True def send_notification(self, user, sender=None, **kwargs): """ An intermediary function for sending an notification email informing a pre-existing, active user that they have been added to a new organization. """ if not user.is_active: return False self.email_message( user, self.notification_subject, self.notification_body, sender, **kwargs ).send() return True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/backends/forms.py0000644000175100001770000000172614527662407024224 0ustar00runnerdocker from django import forms from django.contrib.auth.forms import UserCreationForm class UserRegistrationForm(UserCreationForm): """ Form class for completing a user's registration and activating the User. The class operates on a user model which is assumed to have the required fields of a BaseUserModel """ # TODO(bennylope): Remove this entirely and replace with base class def org_registration_form(org_model): """ Generates a registration ModelForm for the given organization model class """ class OrganizationRegistrationForm(forms.ModelForm): """Form class for creating new organizations owned by new users.""" email = forms.EmailField() class Meta: model = org_model exclude = ("is_active", "users") # def save(self, *args, **kwargs): # self.instance.is_active = False # super().save(*args, **kwargs) return OrganizationRegistrationForm ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/backends/modeled.py0000644000175100001770000001551214527662407024505 0ustar00runnerdocker """ Invitations that use an invitation model """ import email.utils from typing import List # noqa from typing import Optional # noqa from typing import Text # noqa from typing import Tuple # noqa from django.conf import settings from django.contrib.auth.models import AbstractUser # noqa from django.core.mail import EmailMessage from django.http import HttpRequest # noqa from django.http import HttpResponse # noqa from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from django.shortcuts import redirect from django.shortcuts import render from django.template import loader from django.urls import path from django.utils.translation import gettext_lazy as _ from organizations.backends.defaults import InvitationBackend from organizations.backends.forms import UserRegistrationForm from organizations.base import AbstractBaseOrganization # noqa from organizations.base import OrganizationInvitationBase # noqa class ModelInvitation(InvitationBackend): """Invitation backend for model-tracked invitations""" notification_subject = "organizations/email/notification_subject.txt" notification_body = "organizations/email/notification_body.html" invitation_subject = "organizations/email/modeled_invitation_subject.txt" invitation_body = "organizations/email/modeled_invitation_body.html" reminder_subject = "organizations/email/modeled_reminder_subject.txt" reminder_body = "organizations/email/modeled_reminder_body.html" invitation_join_template = "organizations/invitation_join.html" form_class = UserRegistrationForm def __init__(self, org_model=None, namespace=None): super().__init__(org_model=org_model, namespace=namespace) self.invitation_model = ( self.org_model.invitation_model ) # type: OrganizationInvitationBase def get_invitation_queryset(self): """Return this to use a custom queryset that checks for expiration, for example""" return self.invitation_model.objects.all() def get_invitation_accepted_url(self): """Returns the redirect URL after user accepts invitation""" return "/" def get_invitation_accepted_registered_url(self): """Returns the redirect URL after new user accepts invitation""" return self.get_invitation_accepted_url() def activation_router(self, request, guid): """""" invitation = get_object_or_404(self.get_invitation_queryset(), guid=guid) if invitation.invitee: return redirect(self.get_invitation_accepted_url()) if request.user.is_authenticated: return self.activate_existing_user_view(request, invitation) else: return self.activate_new_user_view(request, invitation) def activate_existing_user_view(self, request, invitation): # type: (HttpRequest, OrganizationInvitationBase) -> HttpResponse """""" if request.user == invitation.invited_by: return HttpResponseForbidden(_("This is not your invitation")) if request.method == "POST": invitation.activate(request.user) return redirect(self.get_invitation_accepted_url()) return render( request, self.invitation_join_template, {"invitation": invitation} ) def activate_new_user_view(self, request, invitation): # type: (HttpRequest, OrganizationInvitationBase) -> HttpResponse """""" form = self.get_form(data=request.POST or None) if request.method == "POST" and form.is_valid(): new_user = form.save() # type: AbstractUser invitation.activate(new_user) return redirect(self.get_invitation_accepted_registered_url()) return render( request, self.registration_form_template, {"invitation": invitation, "form": form}, ) def get_urls(self): # type: () -> List[path] return [ path( "/", view=self.activation_router, name="invitations_register" ) ] @property def urls(self): # type: () -> Tuple[List[path], Text] return self.get_urls(), self.namespace or "registration" def invite_by_email(self, email, user, organization, **kwargs): """ Primary interface method by which one user invites another to join Args: email: request: **kwargs: Returns: an invitation instance Raises: MultipleObjectsReturned if multiple matching users are found """ # TODO(bennylope): verify no such user already? # try: # invitee = self.user_model.objects.get(email__iexact=email) # except self.user_model.DoesNotExist: # invitee = None # TODO allow sending just the OrganizationUser instance user_invitation = self.invitation_model.objects.create( invitee_identifier=email.lower(), invited_by=user, organization=organization, ) self.send_invitation(user_invitation) return user_invitation def send_invitation(self, invitation, **kwargs): """ Sends an invitation message for a specific invitation. This could be overridden to do other things, such as sending a confirmation email to the sender. Args: invitation: Returns: """ return self.email_message( invitation.invitee_identifier, self.invitation_subject, self.invitation_body, invitation.invited_by, **kwargs ).send() def email_message( self, recipient, # type: Text subject_template, # type: Text body_template, # type: Text sender=None, # type: Optional[AbstractUser] message_class=EmailMessage, **kwargs ): """ Returns an invitation email message. This can be easily overridden. For instance, to send an HTML message, use the EmailMultiAlternatives message_class and attach the additional conent. """ from_email = "%s %s <%s>" % ( sender.first_name, sender.last_name, email.utils.parseaddr(settings.DEFAULT_FROM_EMAIL)[1], ) reply_to = "%s %s <%s>" % (sender.first_name, sender.last_name, sender.email) headers = {"Reply-To": reply_to} kwargs.update({"sender": sender, "recipient": recipient}) subject_template = loader.get_template(subject_template) body_template = loader.get_template(body_template) subject = subject_template.render( kwargs ).strip() # Remove stray newline characters body = body_template.render(kwargs) return message_class(subject, body, from_email, [recipient], headers=headers) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/base.py0000644000175100001770000003064214527662407022235 0ustar00runnerdocker import uuid from django.conf import settings from django.core.exceptions import FieldDoesNotExist from django.db import models from django.db.models.base import ModelBase from django.urls import reverse from django.utils.translation import gettext_lazy as _ from organizations import signals from organizations.managers import ActiveOrgManager from organizations.managers import OrgManager USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User") # Extracted from six def with_metaclass(meta, *bases): class metaclass(type): def __new__(cls, name, this_bases, d): return meta(name, bases, d) return type.__new__(metaclass, "temporary_class", (), {}) class OrgMeta(ModelBase): """ Base metaclass for dynamically linking related organization models. This is particularly useful for custom organizations that can avoid multitable inheritance and also add additional attributes to the organization users especially. The `module_registry` dictionary is used to track the architecture across different Django apps. If more than one application makes use of these base models, the extended models will share class relationships, which is clearly undesirable. This ensures that the relationships between models within a module using these base classes are from other organization models. """ module_registry = {} def __new__(cls, name, bases, attrs): # noqa # Borrowed from Django-polymorphic # Workaround compatibility issue with with_metaclass() and custom # Django model metaclasses: if not attrs and name == "NewBase": return super(OrgMeta, cls).__new__(cls, name, bases, attrs) base_classes = ["OrgModel", "OrgUserModel", "OrgOwnerModel", "OrgInviteModel"] model = super(OrgMeta, cls).__new__(cls, name, bases, attrs) module = model.__module__ if not cls.module_registry.get(module): cls.module_registry[module] = { "OrgModel": None, "OrgUserModel": None, "OrgOwnerModel": None, "OrgInviteModel": None, } for b in bases: key = None if b.__name__ in ["AbstractOrganization", "OrganizationBase"]: key = "OrgModel" elif b.__name__ in ["AbstractOrganizationUser", "OrganizationUserBase"]: key = "OrgUserModel" elif b.__name__ in ["AbstractOrganizationOwner", "OrganizationOwnerBase"]: key = "OrgOwnerModel" elif b.__name__ in [ "AbstractOrganizationInvitation", "OrganizationInvitationBase", ]: key = "OrgInviteModel" if key: cls.module_registry[module][key] = model if all([cls.module_registry[module][klass] for klass in base_classes]): model.update_org(module) model.update_org_users(module) model.update_org_owner(module) model.update_org_invite(module) return model def update_org(cls, module): """ Adds the `users` field to the organization model """ try: cls.module_registry[module]["OrgModel"]._meta.get_field("users") except FieldDoesNotExist: cls.module_registry[module]["OrgModel"].add_to_class( "users", models.ManyToManyField( USER_MODEL, through=cls.module_registry[module]["OrgUserModel"].__name__, related_name="%(app_label)s_%(class)s", ), ) cls.module_registry[module]["OrgModel"].invitation_model = cls.module_registry[ module ]["OrgInviteModel"] def update_org_users(cls, module): """ Adds the `user` field to the organization user model and the link to the specific organization model. """ try: cls.module_registry[module]["OrgUserModel"]._meta.get_field("user") except FieldDoesNotExist: cls.module_registry[module]["OrgUserModel"].add_to_class( "user", models.ForeignKey( USER_MODEL, related_name="%(app_label)s_%(class)s", on_delete=models.CASCADE, ), ) try: cls.module_registry[module]["OrgUserModel"]._meta.get_field("organization") except FieldDoesNotExist: cls.module_registry[module]["OrgUserModel"].add_to_class( "organization", models.ForeignKey( cls.module_registry[module]["OrgModel"], related_name="organization_users", on_delete=models.CASCADE, ), ) def update_org_owner(cls, module): """ Creates the links to the organization and organization user for the owner. """ try: cls.module_registry[module]["OrgOwnerModel"]._meta.get_field( "organization_user" ) except FieldDoesNotExist: cls.module_registry[module]["OrgOwnerModel"].add_to_class( "organization_user", models.OneToOneField( cls.module_registry[module]["OrgUserModel"], on_delete=models.CASCADE, ), ) try: cls.module_registry[module]["OrgOwnerModel"]._meta.get_field("organization") except FieldDoesNotExist: cls.module_registry[module]["OrgOwnerModel"].add_to_class( "organization", models.OneToOneField( cls.module_registry[module]["OrgModel"], related_name="owner", on_delete=models.CASCADE, ), ) def update_org_invite(cls, module): """ Adds the links to the organization and to the organization user """ try: cls.module_registry[module]["OrgInviteModel"]._meta.get_field("invited_by") except FieldDoesNotExist: cls.module_registry[module]["OrgInviteModel"].add_to_class( "invited_by", models.ForeignKey( USER_MODEL, related_name="%(app_label)s_%(class)s_sent_invitations", on_delete=models.CASCADE, ), ) try: cls.module_registry[module]["OrgInviteModel"]._meta.get_field("invitee") except FieldDoesNotExist: cls.module_registry[module]["OrgInviteModel"].add_to_class( "invitee", models.ForeignKey( USER_MODEL, null=True, blank=True, related_name="%(app_label)s_%(class)s_invitations", on_delete=models.CASCADE, ), ) try: cls.module_registry[module]["OrgInviteModel"]._meta.get_field( "organization" ) except FieldDoesNotExist: cls.module_registry[module]["OrgInviteModel"].add_to_class( "organization", models.ForeignKey( cls.module_registry[module]["OrgModel"], related_name="organization_invites", on_delete=models.CASCADE, ), ) class AbstractBaseOrganization(models.Model): """ The umbrella object with which users can be associated. An organization can have multiple users but only one who can be designated the owner user. """ name = models.CharField(max_length=200, help_text=_("The name of the organization")) is_active = models.BooleanField(default=True) objects = OrgManager() active = ActiveOrgManager() class Meta: abstract = True ordering = ["name"] def __str__(self): return self.name @property def user_relation_name(self): """ Returns the string name of the related name to the user. This provides a consistent interface across different organization model classes. """ return "{0}_{1}".format( self._meta.app_label.lower(), self.__class__.__name__.lower() ) def is_member(self, user): return True if user in self.users.all() else False class OrganizationBase(with_metaclass(OrgMeta, AbstractBaseOrganization)): class Meta(AbstractBaseOrganization.Meta): abstract = True @property def _org_user_model(self): return self.__class__.module_registry[self.__class__.__module__]["OrgUserModel"] def add_user(self, user, **kwargs): org_user = self._org_user_model.objects.create( user=user, organization=self, **kwargs ) signals.user_added.send(sender=self, user=user) return org_user class AbstractBaseOrganizationUser(models.Model): """ ManyToMany through field relating Users to Organizations. It is possible for a User to be a member of multiple organizations, so this class relates the OrganizationUser to the User model using a ForeignKey relationship, rather than a OneToOne relationship. Authentication and general user information is handled by the User class and the contrib.auth application. """ class Meta: abstract = True ordering = ["organization", "user"] unique_together = ("user", "organization") def __str__(self): return "{name} {org}".format( name=self.name if self.user.is_active else self.user.email, org=self.organization.name, ) @property def name(self): """ Returns the connected user's full name or string representation if the full name method is unavailable (e.g. on a custom user class). """ try: return self.user.get_full_name() except AttributeError: return str(self.user) class OrganizationUserBase(with_metaclass(OrgMeta, AbstractBaseOrganizationUser)): class Meta(AbstractBaseOrganizationUser.Meta): abstract = True class AbstractBaseOrganizationOwner(models.Model): """ Each organization must have one and only one organization owner. """ class Meta: abstract = True def __str__(self): return "{0}: {1}".format(self.organization, self.organization_user) class OrganizationOwnerBase(with_metaclass(OrgMeta, AbstractBaseOrganizationOwner)): class Meta(AbstractBaseOrganizationOwner.Meta): abstract = True class AbstractBaseInvitation(models.Model): """ Tracks invitations to organizations This tracks *users* specifically, rather than OrganizationUsers, as it's considered *more critical* to know who invited or joined even if they are no longer members of the organization. """ guid = models.UUIDField(editable=False) invitee_identifier = models.CharField( max_length=1000, help_text=_( "The contact identifier for the invitee, email, phone number, social media handle, etc." ), ) class Meta: abstract = True def __str__(self): return "{0}: {1}".format(self.organization, self.invitee_identifier) def save(self, **kwargs): if not self.guid: self.guid = str(uuid.uuid4()) return super().save(**kwargs) def get_absolute_url(self): """Returns the invitation URL""" return reverse("invitations_register", kwargs={"guid": self.guid}) def activation_kwargs(self): """Override this to add kwargs to add_user on activation""" return {} def activate(self, user): """ Updates the `invitee` value and saves the instance Provided as a way of extending the behavior. Args: user: the newly created user Returns: the linking organization user """ org_user = self.organization.add_user(user, **self.activation_kwargs()) self.invitee = user self.save() return org_user def invitation_token(self): """ Returns a unique token for the user Hash based on identification, account id, time invitited, and secret key of site """ raise NotImplementedError class OrganizationInvitationBase(with_metaclass(OrgMeta, AbstractBaseInvitation)): class Meta(AbstractBaseInvitation.Meta): abstract = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/base_admin.py0000644000175100001770000000111314527662407023374 0ustar00runnerdocker from django.contrib import admin class BaseOwnerInline(admin.StackedInline): raw_id_fields = ("organization_user",) class BaseOrganizationAdmin(admin.ModelAdmin): list_display = ["name", "is_active"] prepopulated_fields = {"slug": ("name",)} search_fields = ["name"] list_filter = ("is_active",) class BaseOrganizationUserAdmin(admin.ModelAdmin): list_display = ["user", "organization", "is_admin"] raw_id_fields = ("user", "organization") class BaseOrganizationOwnerAdmin(admin.ModelAdmin): raw_id_fields = ("organization_user", "organization") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/exceptions.py0000644000175100001770000000052014527662407023474 0ustar00runnerdocker class OwnershipRequired(Exception): """ Exception to raise if the owner is being removed before the organization. """ pass class OrganizationMismatch(Exception): """ Exception to raise if an organization user from a different organization is assigned to be an organization's owner. """ pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/fields.py0000644000175100001770000000576014527662407022574 0ustar00runnerdocker""" Most of this code extracted and borrowed from django-model-utils Copyright (c) 2009-2015, Carl Meyer and contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from importlib import import_module from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import models from django.utils.timezone import now class AutoCreatedField(models.DateTimeField): """ A DateTimeField that automatically populates itself at object creation. By default, sets editable=False, default=datetime.now. """ def __init__(self, *args, **kwargs): kwargs.setdefault("editable", False) kwargs.setdefault("default", now) super().__init__(*args, **kwargs) class AutoLastModifiedField(AutoCreatedField): """ A DateTimeField that updates itself on each save() of the model. By default, sets editable=False and default=datetime.now. """ def pre_save(self, model_instance, add): value = now() setattr(model_instance, self.attname, value) return value ORGS_SLUGFIELD = getattr( settings, "ORGS_SLUGFIELD", "django_extensions.db.fields.AutoSlugField" ) try: module, klass = ORGS_SLUGFIELD.rsplit(".", 1) BaseSlugField = getattr(import_module(module), klass) except (ImportError, ValueError, AttributeError): raise ImproperlyConfigured( "Your SlugField class, '{0}', is improperly defined. " "See the documentation and install an auto slug field".format(ORGS_SLUGFIELD) ) class SlugField(BaseSlugField): """Class redefinition for migrations""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/forms.py0000644000175100001770000001423514527662407022451 0ustar00runnerdocker from django import forms from django.contrib.auth import get_user_model from django.contrib.sites.shortcuts import get_current_site from django.utils.translation import gettext_lazy as _ from organizations.backends import invitation_backend from organizations.models import Organization from organizations.models import OrganizationUser from organizations.utils import create_organization class OrganizationForm(forms.ModelForm): """Form class for updating Organizations""" owner = forms.ModelChoiceField(OrganizationUser.objects.all()) def __init__(self, request, *args, **kwargs): self.request = request super().__init__(*args, **kwargs) self.fields["owner"].queryset = self.instance.organization_users.filter( is_admin=True, user__is_active=True ) self.fields["owner"].initial = self.instance.owner.organization_user class Meta: model = Organization exclude = ("users", "is_active") def save(self, commit=True): if self.instance.owner.organization_user != self.cleaned_data["owner"]: self.instance.change_owner(self.cleaned_data["owner"]) return super().save(commit=commit) def clean_owner(self): owner = self.cleaned_data["owner"] if owner != self.instance.owner.organization_user: if self.request.user != self.instance.owner.organization_user.user: raise forms.ValidationError( _("Only the organization owner can change ownerhip") ) return owner class OrganizationUserForm(forms.ModelForm): """Form class for updating OrganizationUsers""" class Meta: model = OrganizationUser exclude = ("organization", "user") def clean_is_admin(self): is_admin = self.cleaned_data["is_admin"] if ( self.instance.organization.owner.organization_user == self.instance and not is_admin ): raise forms.ValidationError(_("The organization owner must be an admin")) return is_admin class OrganizationUserAddForm(forms.ModelForm): """Form class for adding OrganizationUsers to an existing Organization""" email = forms.EmailField(max_length=75) def __init__(self, request, organization, *args, **kwargs): self.request = request self.organization = organization super().__init__(*args, **kwargs) class Meta: model = OrganizationUser exclude = ("user", "organization") def save(self, *args, **kwargs): """ The save method should create a new OrganizationUser linking the User matching the provided email address. If not matching User is found it should kick off the registration process. It needs to create a User in order to link it to the Organization. """ try: user = get_user_model().objects.get( email__iexact=self.cleaned_data["email"] ) except get_user_model().DoesNotExist: user = invitation_backend().invite_by_email( self.cleaned_data["email"], **{ "domain": get_current_site(self.request), "organization": self.organization, "sender": self.request.user, } ) # Send a notification email to this user to inform them that they # have been added to a new organization. invitation_backend().send_notification( user, **{ "domain": get_current_site(self.request), "organization": self.organization, "sender": self.request.user, } ) return OrganizationUser.objects.create( user=user, organization=self.organization, is_admin=self.cleaned_data["is_admin"], ) def clean_email(self): email = self.cleaned_data["email"] if self.organization.users.filter(email__iexact=email).exists(): raise forms.ValidationError( _("There is already an organization " "member with this email address!") ) if get_user_model().objects.filter(email__iexact=email).count() > 1: raise forms.ValidationError( _("This email address has been used multiple times.") ) return email class OrganizationAddForm(forms.ModelForm): """ Form class for creating a new organization, complete with new owner, including a User instance, OrganizationUser instance, and OrganizationOwner instance. """ email = forms.EmailField( max_length=75, help_text=_("The email address for the account owner") ) def __init__(self, request, *args, **kwargs): self.request = request super().__init__(*args, **kwargs) class Meta: model = Organization exclude = ("users", "is_active") def save(self, **kwargs): """ Create the organization, then get the user, then make the owner. """ is_active = True try: user = get_user_model().objects.get(email=self.cleaned_data["email"]) except get_user_model().DoesNotExist: # TODO(bennylope): look into hooks for alt. registration systems here user = invitation_backend().invite_by_email( self.cleaned_data["email"], **{ "domain": get_current_site(self.request), "organization": self.cleaned_data["name"], "sender": self.request.user, "created": True, } ) is_active = False return create_organization( user, self.cleaned_data["name"], self.cleaned_data["slug"], is_active=is_active, ) class SignUpForm(forms.Form): """ Form class for signing up a new user and new account. """ name = forms.CharField(max_length=50, help_text=_("The name of the organization")) slug = forms.SlugField( max_length=50, help_text=_("The name in all lowercase, suitable for URL identification"), ) email = forms.EmailField() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/managers.py0000644000175100001770000000063514527662407023117 0ustar00runnerdocker from django.db import models class OrgManager(models.Manager): def get_for_user(self, user): return self.get_queryset().filter(users=user) class ActiveOrgManager(OrgManager): """ A more useful extension of the default manager which returns querysets including only active organizations """ def get_queryset(self): return super().get_queryset().filter(is_active=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7141752 django-organizations-2.3.1/src/organizations/migrations/0000755000175100001770000000000014527662421023114 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0001_initial.py0000644000175100001770000001370614527662407025572 0ustar00runnerdocker# Generated by Django 2.0 on 2017-12-05 00:17 import django.db.models.deletion import django.utils.timezone from django.conf import settings from django.db import migrations from django.db import models import organizations.base import organizations.fields class Migration(migrations.Migration): initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( name="Organization", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "name", models.CharField( help_text="The name of the organization", max_length=200 ), ), ("is_active", models.BooleanField(default=True)), ( "created", organizations.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False ), ), ( "modified", organizations.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False ), ), ( "slug", organizations.fields.SlugField( editable=True, help_text="The name in all lowercase, suitable for URL identification", max_length=200, populate_from="name", unique=True, ), ), ], options={ "verbose_name": "organization", "verbose_name_plural": "organizations", "ordering": ["name"], "abstract": False, }, ), migrations.CreateModel( name="OrganizationOwner", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "created", organizations.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False ), ), ( "modified", organizations.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False ), ), ( "organization", models.OneToOneField( on_delete=django.db.models.deletion.CASCADE, related_name="owner", to="organizations.Organization", ), ), ], options={ "verbose_name": "organization owner", "verbose_name_plural": "organization owners", "abstract": False, }, ), migrations.CreateModel( name="OrganizationUser", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "created", organizations.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False ), ), ( "modified", organizations.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False ), ), ("is_admin", models.BooleanField(default=False)), ( "organization", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="organization_users", to="organizations.Organization", ), ), ( "user", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="organizations_organizationuser", to=settings.AUTH_USER_MODEL, ), ), ], options={ "verbose_name": "organization user", "verbose_name_plural": "organization users", "ordering": ["organization", "user"], "abstract": False, }, ), migrations.AddField( model_name="organizationowner", name="organization_user", field=models.OneToOneField( on_delete=django.db.models.deletion.CASCADE, to="organizations.OrganizationUser", ), ), migrations.AddField( model_name="organization", name="users", field=models.ManyToManyField( related_name="organizations_organization", through="organizations.OrganizationUser", to=settings.AUTH_USER_MODEL, ), ), migrations.AlterUniqueTogether( name="organizationuser", unique_together={("user", "organization")} ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0002_model_update.py0000644000175100001770000000113414527662407026574 0ustar00runnerdockerfrom django.db import migrations import organizations.fields class Migration(migrations.Migration): dependencies = [("organizations", "0001_initial")] operations = [ migrations.AlterField( model_name="organization", name="slug", field=organizations.fields.SlugField( blank=True, editable=False, help_text="The name in all lowercase, suitable for URL identification", max_length=200, populate_from=("name",), unique=True, ), ) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0003_field_fix_and_editable.py0000644000175100001770000000110114527662407030531 0ustar00runnerdockerfrom django.db import migrations import organizations.fields class Migration(migrations.Migration): dependencies = [("organizations", "0002_model_update")] operations = [ migrations.AlterField( model_name="organization", name="slug", field=organizations.fields.SlugField( editable=True, help_text="The name in all lowercase, suitable for URL identification", max_length=200, populate_from="name", unique=True, ), ) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0004_organizationinvitation.py0000644000175100001770000000542514527662407030754 0ustar00runnerdocker# Generated by Django 2.0.5 on 2019-06-27 00:54 import django.db.models.deletion import django.utils.timezone from django.conf import settings from django.db import migrations from django.db import models import organizations.base import organizations.fields class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("organizations", "0003_field_fix_and_editable"), ] operations = [ migrations.CreateModel( name="OrganizationInvitation", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("guid", models.UUIDField(editable=False)), ( "invitee_identifier", models.CharField( help_text="The contact identifier for the invitee, email, phone number, social media handle, etc.", max_length=1000, ), ), ( "created", organizations.fields.AutoCreatedField( default=django.utils.timezone.now, editable=False ), ), ( "modified", organizations.fields.AutoLastModifiedField( default=django.utils.timezone.now, editable=False ), ), ( "invited_by", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="organizations_organizationinvitation_sent_invitations", to=settings.AUTH_USER_MODEL, ), ), ( "invitee", models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="organizations_organizationinvitation_invitations", to=settings.AUTH_USER_MODEL, ), ), ( "organization", models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="organization_invites", to="organizations.Organization", ), ), ], options={"abstract": False}, ) ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0005_alter_organization_users_and_more.py0000644000175100001770000000345214527662407033122 0ustar00runnerdocker# Generated by Django 4.0.7 on 2022-09-30 17:00 from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ("auth", "0012_alter_user_first_name_max_length"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("organizations", "0004_organizationinvitation"), ] operations = [ migrations.AlterField( model_name="organization", name="users", field=models.ManyToManyField( related_name="%(app_label)s_%(class)s", through="organizations.OrganizationUser", to=settings.AUTH_USER_MODEL, ), ), migrations.AlterField( model_name="organizationinvitation", name="invited_by", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="%(app_label)s_%(class)s_sent_invitations", to=settings.AUTH_USER_MODEL, ), ), migrations.AlterField( model_name="organizationinvitation", name="invitee", field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name="%(app_label)s_%(class)s_invitations", to=settings.AUTH_USER_MODEL, ), ), migrations.AlterField( model_name="organizationuser", name="user", field=models.ForeignKey( on_delete=django.db.models.deletion.CASCADE, related_name="%(app_label)s_%(class)s", to=settings.AUTH_USER_MODEL, ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/0006_alter_organization_slug.py0000644000175100001770000000126314527662407031066 0ustar00runnerdocker# Generated by Django 4.0.7 on 2022-10-13 02:45 from django.db import migrations import organizations.fields class Migration(migrations.Migration): dependencies = [ ("organizations", "0005_alter_organization_users_and_more"), ] operations = [ migrations.AlterField( model_name="organization", name="slug", field=organizations.fields.SlugField( blank=True, editable=False, help_text="The name in all lowercase, suitable for URL identification", max_length=200, populate_from="name", unique=True, ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/migrations/__init__.py0000644000175100001770000000000014527662407025217 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/models.py0000644000175100001770000000161114527662407022600 0ustar00runnerdocker from organizations.abstract import AbstractOrganization from organizations.abstract import AbstractOrganizationInvitation from organizations.abstract import AbstractOrganizationOwner from organizations.abstract import AbstractOrganizationUser class Organization(AbstractOrganization): """ Default Organization model. """ class Meta(AbstractOrganization.Meta): abstract = False class OrganizationUser(AbstractOrganizationUser): """ Default OrganizationUser model. """ class Meta(AbstractOrganizationUser.Meta): abstract = False class OrganizationOwner(AbstractOrganizationOwner): """ Default OrganizationOwner model. """ class Meta(AbstractOrganizationOwner.Meta): abstract = False class OrganizationInvitation(AbstractOrganizationInvitation): class Meta(AbstractOrganizationInvitation.Meta): abstract = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/signals.py0000644000175100001770000000027714527662407022764 0ustar00runnerdocker import django.dispatch user_added = django.dispatch.Signal() user_removed = django.dispatch.Signal() invitation_accepted = django.dispatch.Signal() owner_changed = django.dispatch.Signal() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7141752 django-organizations-2.3.1/src/organizations/templates/0000755000175100001770000000000014527662421022736 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7181752 django-organizations-2.3.1/src/organizations/templates/organizations/0000755000175100001770000000000014527662421025625 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7181752 django-organizations-2.3.1/src/organizations/templates/organizations/email/0000755000175100001770000000000014527662421026714 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/activation_body.html0000644000175100001770000000000014527662407032752 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/activation_subject.txt0000644000175100001770000000000014527662407033327 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/invitation_body.html0000644000175100001770000000047514527662407033015 0ustar00runnerdockerYou've been invited to join {{ organization|safe }} on {{ domain.name }} by {{ sender.first_name|safe }} {{ sender.last_name|safe }}. Follow this link to create your user account. http://{{ domain.domain }}{% url "invitations_register" user.pk token %} If you are unsure about this link please contact the sender. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/invitation_subject.txt0000644000175100001770000000006614527662407033366 0ustar00runnerdocker{% spaceless %}You've been invited!{% endspaceless %} ././@PaxHeader0000000000000000000000000000020700000000000010214 xustar00113 path=django-organizations-2.3.1/src/organizations/templates/organizations/email/modeled_invitation_body.html 22 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/modeled_invitation_body.h0000644000175100001770000000043714527662407033767 0ustar00runnerdockerYou've been invited to join {{ organization|safe }} on {{ domain.name }} by {{ sender.first_name|safe }} {{ sender.last_name|safe }}. Follow this link to create your user account. http://{{ domain.domain }}{{ invitation }} If you are unsure about this link please contact the sender. ././@PaxHeader0000000000000000000000000000021100000000000010207 xustar00115 path=django-organizations-2.3.1/src/organizations/templates/organizations/email/modeled_invitation_subject.txt 22 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/modeled_invitation_subjec0000644000175100001770000000006614527662407034055 0ustar00runnerdocker{% spaceless %}You've been invited!{% endspaceless %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/notification_body.html0000644000175100001770000000045714527662407033317 0ustar00runnerdockerYou've been added to the organization {{ organization|safe }} on {{ domain.name }} by {{ sender.full_name|safe }}.` Follow the link below to view this organization. http://{{ domain.domain }}{% url "organization_detail" organization.pk %} If you are unsure about this link please contact the sender. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/notification_subject.txt0000644000175100001770000000010614527662407033663 0ustar00runnerdocker{% spaceless %}You've been added to an organization{% endspaceless %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/reminder_body.html0000644000175100001770000000047514527662407032436 0ustar00runnerdockerYou've been invited to join {{ organization|safe }} on {{ domain.name }} by {{ sender.first_name|safe }} {{ sender.last_name|safe }}. Follow this link to create your user account. http://{{ domain.domain }}{% url "invitations_register" user.pk token %} If you are unsure about this link please contact the sender. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/email/reminder_subject.txt0000644000175100001770000000002014527662407032775 0ustar00runnerdockerJust a reminder ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/invitation_join.html0000644000175100001770000000024114527662407031717 0ustar00runnerdocker{% load i18n %} {% trans "Would you like to join?" %}
{% csrf_token %}
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/login.html0000644000175100001770000000065714527662407027637 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

Log in to your dashboard

{% csrf_token %} {{ form }}

{% trans "Forgotten your password?" %} {% trans "Reset it" %}.

{% endblock %} ././@PaxHeader0000000000000000000000000000020500000000000010212 xustar00111 path=django-organizations-2.3.1/src/organizations/templates/organizations/organization_confirm_delete.html 22 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organization_confirm_delete.htm0000644000175100001770000000046114527662407034107 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

{{ organization }}

{% trans "Are you sure you want to delete this organization?" %}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organization_detail.html0000644000175100001770000000073014527662407032545 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% load org_tags %} {% block content %}

{{ organization }}

{% organization_users organization %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organization_form.html0000644000175100001770000000032714527662407032250 0ustar00runnerdocker{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organization_list.html0000644000175100001770000000043314527662407032256 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

{% trans "organizations" %}

{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organization_users.html0000644000175100001770000000043514527662407032446 0ustar00runnerdocker{% load i18n %}
    {% for organization_user in organization_users %}
  • {{ organization_user }} {% if not organization_user.user.is_active %}{% trans "Send reminder" %}{% endif %}
  • {% endfor %}
././@PaxHeader0000000000000000000000000000021100000000000010207 xustar00115 path=django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_confirm_delete.html 22 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_confirm_delete0000644000175100001770000000032714527662407034220 0ustar00runnerdocker{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_detail.html0000644000175100001770000000115314527662407033444 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

{{ organization_user }} @ {{ organization }}

{% if user == organization_user.user %}{% trans "This is you" %}!{% endif %}
  • {% trans "Name" %}: {{ organization_user.full_name }}
  • {% trans "Email" %}: {{ organization_user.user.email }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_form.html0000644000175100001770000000064214527662407033147 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% block content %} {% if profile %}

{% trans "Update your profile" %}

{% else %}

{{ organization_user }} @ {{ organization }}

{% endif %} {% if user == organization_user.user %}{% trans "This is you" %}!{% endif %}
{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_list.html0000644000175100001770000000045314527662407033157 0ustar00runnerdocker{% extends "organizations_base.html" %} {% load i18n %} {% load org_tags %} {% block content %}

{{ organization }}'s Members

{% organization_users organization %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/organizationuser_remind.html0000644000175100001770000000033614527662407033462 0ustar00runnerdocker{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/register_form.html0000644000175100001770000000021614527662407031365 0ustar00runnerdocker
{% csrf_token %} {{ form }}
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations/register_success.html0000644000175100001770000000004614527662407032073 0ustar00runnerdocker{% load i18n %} {% trans "Thanks!" %} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templates/organizations_base.html0000644000175100001770000000004214527662407027505 0ustar00runnerdocker{% block content %}{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7181752 django-organizations-2.3.1/src/organizations/templatetags/0000755000175100001770000000000014527662421023432 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templatetags/__init__.py0000644000175100001770000000000014527662407025535 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/templatetags/org_tags.py0000644000175100001770000000067714527662407025627 0ustar00runnerdocker from django import template register = template.Library() @register.inclusion_tag("organizations/organization_users.html", takes_context=True) def organization_users(context, org): context.update({"organization_users": org.organization_users.all()}) return context @register.filter def is_admin(org, user): return org.is_admin(user) @register.filter def is_owner(org, user): return org.owner.organization_user.user == user ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/urls.py0000644000175100001770000000646714527662407022320 0ustar00runnerdocker from django.contrib.auth.decorators import login_required from django.urls import include from django.urls import path from organizations.views import default as views # app_name = "organizations" urlpatterns = [ path( "", view=login_required(views.OrganizationList.as_view()), name="organization_list", ), path( "add/", view=login_required(views.OrganizationCreate.as_view()), name="organization_add", ), path( "/", include( [ path( "", view=login_required(views.OrganizationDetail.as_view()), name="organization_detail", ), path( "edit/", view=login_required(views.OrganizationUpdate.as_view()), name="organization_edit", ), path( "delete/", view=login_required(views.OrganizationDelete.as_view()), name="organization_delete", ), path( "people/", include( [ path( "", view=login_required( views.OrganizationUserList.as_view() ), name="organization_user_list", ), path( "add/", view=login_required( views.OrganizationUserCreate.as_view() ), name="organization_user_add", ), path( "/remind/", view=login_required( views.OrganizationUserRemind.as_view() ), name="organization_user_remind", ), path( "/", view=login_required( views.OrganizationUserDetail.as_view() ), name="organization_user_detail", ), path( "/edit/", view=login_required( views.OrganizationUserUpdate.as_view() ), name="organization_user_edit", ), path( "/delete/", view=login_required( views.OrganizationUserDelete.as_view() ), name="organization_user_delete", ), ] ), ), ] ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/utils.py0000644000175100001770000000512314527662407022457 0ustar00runnerdocker from itertools import chain def default_org_model(): """Encapsulates importing the concrete model""" from organizations.models import Organization return Organization def model_field_names(model): """ Returns a list of field names in the model Direct from Django upgrade migration guide. """ return list( set( chain.from_iterable( (field.name, field.attname) if hasattr(field, "attname") else (field.name,) for field in model._meta.get_fields() if not (field.many_to_one and field.related_model is None) ) ) ) def create_organization( user, name, slug=None, is_active=None, org_defaults=None, org_user_defaults=None, **kwargs ): """ Returns a new organization, also creating an initial organization user who is the owner. The specific models can be specified if a custom organization app is used. The simplest way would be to use a partial. >>> from organizations.utils import create_organization >>> from myapp.models import Account >>> from functools import partial >>> create_account = partial(create_organization, model=Account) """ org_model = ( kwargs.pop("model", None) or kwargs.pop("org_model", None) or default_org_model() ) kwargs.pop("org_user_model", None) # Discard deprecated argument org_owner_model = org_model.owner.related.related_model org_user_model = org_model.organization_users.rel.related_model if org_defaults is None: org_defaults = {} if org_user_defaults is None: if "is_admin" in model_field_names(org_user_model): org_user_defaults = {"is_admin": True} else: org_user_defaults = {} if slug is not None: org_defaults.update({"slug": slug}) if is_active is not None: org_defaults.update({"is_active": is_active}) org_defaults.update({"name": name}) organization = org_model.objects.create(**org_defaults) org_user_defaults.update({"organization": organization, "user": user}) new_user = org_user_model.objects.create(**org_user_defaults) org_owner_model.objects.create( organization=organization, organization_user=new_user ) return organization def model_field_attr(model, model_field, attr): """ Returns the specified attribute for the specified field on the model class. """ fields = dict([(field.name, field) for field in model._meta.fields]) return getattr(fields[model_field], attr) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/src/organizations/views/0000755000175100001770000000000014527662421022075 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/views/__init__.py0000644000175100001770000000000014527662407024200 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/views/base.py0000644000175100001770000002101614527662407023365 0ustar00runnerdocker from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponseGone from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext as _ from django.views.generic import CreateView from django.views.generic import DeleteView from django.views.generic import DetailView from django.views.generic import FormView from django.views.generic import ListView from django.views.generic import UpdateView from organizations.backends import invitation_backend from organizations.backends import registration_backend from organizations.forms import OrganizationAddForm from organizations.forms import OrganizationForm from organizations.forms import OrganizationUserAddForm from organizations.forms import OrganizationUserForm from organizations.forms import SignUpForm from organizations.utils import create_organization from organizations.views.mixins import OrganizationMixin from organizations.views.mixins import OrganizationUserMixin class BaseOrganizationList(ListView): context_object_name = "organizations" def get_queryset(self): return self.org_model.active.filter(users=self.request.user) class BaseOrganizationDetail(OrganizationMixin, DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["organization_users"] = self.organization.organization_users.all() context["organization"] = self.organization return context class BaseOrganizationCreate(CreateView): form_class = OrganizationAddForm template_name = "organizations/organization_form.html" def get_success_url(self): return reverse("organization_list") def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs.update({"request": self.request}) return kwargs class BaseOrganizationUpdate(OrganizationMixin, UpdateView): form_class = OrganizationForm def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs.update({"request": self.request}) return kwargs class BaseOrganizationDelete(OrganizationMixin, DeleteView): def get_success_url(self): return reverse("organization_list") class BaseOrganizationUserList(OrganizationMixin, ListView): def get(self, request, *args, **kwargs): self.organization = self.get_organization() self.object_list = self.organization.organization_users.all() context = self.get_context_data( object_list=self.object_list, organization_users=self.object_list, organization=self.organization, ) return self.render_to_response(context) class BaseOrganizationUserDetail(OrganizationUserMixin, DetailView): pass class BaseOrganizationUserCreate(OrganizationMixin, CreateView): form_class = OrganizationUserAddForm template_name = "organizations/organizationuser_form.html" def get_success_url(self): return reverse( "organization_user_list", kwargs={"organization_pk": self.object.organization.pk}, ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs.update({"organization": self.organization, "request": self.request}) return kwargs def get(self, request, *args, **kwargs): self.organization = self.get_object() return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.organization = self.get_object() return super().post(request, *args, **kwargs) class BaseOrganizationUserRemind(OrganizationUserMixin, DetailView): """ Reminder view for already-linked org users This is only applicable for invitation backends using the strategy the original "default" backend uses, which is to immediately add existing users to the organization after invite, but leave new users inactive until confirmation. """ template_name = "organizations/organizationuser_remind.html" # TODO move to invitations backend? def get_success_url(self): return reverse( "organization_user_list", kwargs={"organization_pk": self.object.organization.pk}, ) def get(self, request, *args, **kwargs): self.object = self.get_object() if self.object.user.is_active: return HttpResponseGone(_("User is already active")) return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() if self.object.user.is_active: return HttpResponseGone(_("User is already active")) invitation_backend().send_reminder( self.object.user, **{ "domain": get_current_site(self.request), "organization": self.organization, "sender": request.user, } ) return redirect(self.get_success_url()) class BaseOrganizationUserUpdate(OrganizationUserMixin, UpdateView): form_class = OrganizationUserForm class BaseOrganizationUserDelete(OrganizationUserMixin, DeleteView): def get_success_url(self): return reverse( "organization_user_list", kwargs={"organization_pk": self.object.organization.pk}, ) class OrganizationSignup(FormView): """ View that allows unregistered users to create an organization account. It simply processes the form and then calls the specified registration backend. """ form_class = SignUpForm template_name = "organizations/signup_form.html" # TODO get success from backend, because some backends may do something # else, like require verification backend = registration_backend() def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: return redirect("organization_add") return super().dispatch(request, *args, **kwargs) def get_success_url(self): if getattr(self, "success_url", None): return self.success_url raise ImproperlyConfigured( "{cls} must either have a `success_url` attribute defined" "or override `get_success_url`".format(cls=self.__class__.__name__) ) def form_valid(self, form): """ Register user and create the organization """ user = self.backend.register_by_email(form.cleaned_data["email"]) create_organization( user=user, name=form.cleaned_data["name"], slug=form.cleaned_data["slug"], is_active=False, ) return redirect(self.get_success_url()) class ViewFactory: """ A class that can create a faked 'module' with model specific views These views have NO access control applied. """ def __init__(self, org_model): self.org_model = org_model @property def OrganizationList(self): klass = BaseOrganizationList klass.org_model = self.org_model return klass @property def OrganizationDetail(self): klass = BaseOrganizationDetail klass.org_model = self.org_model return klass @property def OrganizationCreate(self): klass = BaseOrganizationCreate klass.org_model = self.org_model return klass @property def OrganizationUpdate(self): klass = BaseOrganizationUpdate klass.org_model = self.org_model return klass @property def OrganizationDelete(self): klass = BaseOrganizationDelete klass.org_model = self.org_model return klass @property def OrganizationUserList(self): klass = BaseOrganizationUserList klass.org_model = self.org_model return klass @property def OrganizationUserDetail(self): klass = BaseOrganizationUserDetail klass.org_model = self.org_model return klass @property def OrganizationUserUpdate(self): klass = BaseOrganizationUserUpdate klass.org_model = self.org_model return klass @property def OrganizationUserCreate(self): klass = BaseOrganizationUserCreate klass.org_model = self.org_model return klass @property def OrganizationUserDelete(self): klass = BaseOrganizationUserDelete klass.org_model = self.org_model return klass @property def OrganizationUserRemind(self): klass = BaseOrganizationUserRemind klass.org_model = self.org_model return klass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/views/default.py0000644000175100001770000000242414527662407024101 0ustar00runnerdocker from organizations.models import Organization from organizations.views.base import ViewFactory from organizations.views.mixins import AdminRequiredMixin from organizations.views.mixins import MembershipRequiredMixin from organizations.views.mixins import OwnerRequiredMixin bases = ViewFactory(Organization) class OrganizationList(bases.OrganizationList): pass class OrganizationCreate(bases.OrganizationCreate): """ Allows any user to create a new organization. """ pass class OrganizationDetail(MembershipRequiredMixin, bases.OrganizationDetail): pass class OrganizationUpdate(AdminRequiredMixin, bases.OrganizationUpdate): pass class OrganizationDelete(OwnerRequiredMixin, bases.OrganizationDelete): pass class OrganizationUserList(MembershipRequiredMixin, bases.OrganizationUserList): pass class OrganizationUserDetail(AdminRequiredMixin, bases.OrganizationUserDetail): pass class OrganizationUserUpdate(AdminRequiredMixin, bases.OrganizationUserUpdate): pass class OrganizationUserCreate(AdminRequiredMixin, bases.OrganizationUserCreate): pass class OrganizationUserRemind(AdminRequiredMixin, bases.OrganizationUserRemind): pass class OrganizationUserDelete(AdminRequiredMixin, bases.OrganizationUserDelete): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/src/organizations/views/mixins.py0000644000175100001770000000735114527662407023770 0ustar00runnerdocker from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from organizations.models import Organization from organizations.models import OrganizationUser class OrganizationMixin: """Mixin used like a SingleObjectMixin to fetch an organization""" org_model = Organization org_context_name = "organization" def get_org_model(self): return self.org_model def get_context_data(self, **kwargs): kwargs.update({self.org_context_name: self.organization}) return super().get_context_data(**kwargs) @cached_property def organization(self): organization_pk = self.kwargs.get("organization_pk", None) return get_object_or_404(self.get_org_model(), pk=organization_pk) def get_object(self): return self.organization get_organization = get_object # Now available when `get_object` is overridden class OrganizationUserMixin(OrganizationMixin): """Mixin used like a SingleObjectMixin to fetch an organization user""" user_model = OrganizationUser org_user_context_name = "organization_user" def get_user_model(self): return self.user_model def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs.update( { self.org_user_context_name: self.object, self.org_context_name: self.object.organization, } ) return kwargs @cached_property def organization_user(self): """ Returns the OrganizationUser object This is fetched based on the primary keys for both the organization and the organization user. """ organization_pk = self.kwargs.get("organization_pk", None) user_pk = self.kwargs.get("user_pk", None) return get_object_or_404( self.get_user_model().objects.select_related(), user__pk=user_pk, organization__pk=organization_pk, ) def get_object(self): """Proxy for the base class interface This can be called all day long and the object is queried once. """ return self.organization_user class MembershipRequiredMixin: """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs if ( not self.organization.is_member(request.user) and not request.user.is_superuser ): raise PermissionDenied(_("Wrong organization")) return super().dispatch(request, *args, **kwargs) class AdminRequiredMixin: """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs if ( not self.organization.is_admin(request.user) and not request.user.is_superuser ): raise PermissionDenied(_("Sorry, admins only")) return super().dispatch(request, *args, **kwargs) class OwnerRequiredMixin: """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs if ( self.organization.owner.organization_user.user != request.user and not request.user.is_superuser ): raise PermissionDenied(_("You are not the organization owner")) return super().dispatch(request, *args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1700750608.7221751 django-organizations-2.3.1/tests/0000755000175100001770000000000014527662421016424 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_fields.py0000644000175100001770000000124214527662407021306 0ustar00runnerdocker""" Tests for configurable fields """ import importlib from django.core.exceptions import ImproperlyConfigured import pytest from organizations import fields def test_misconfigured_autoslug_cannot_import(settings): settings.ORGS_SLUGFIELD = "not.AModel" with pytest.raises(ImproperlyConfigured): importlib.reload(fields) def test_misconfigured_autoslug_incorrect_class(settings): settings.ORGS_SLUGFIELD = "autoslug.AutoSlug" with pytest.raises(ImproperlyConfigured): importlib.reload(fields) def test_misconfigured_autoslug_bad_notation(settings): settings.ORGS_SLUGFIELD = "autoslug.AutoSlugField" importlib.reload(fields) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_forms.py0000644000175100001770000001437014527662407021174 0ustar00runnerdockerfrom django.contrib.auth import get_user_model from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from organizations.forms import OrganizationAddForm from organizations.forms import OrganizationForm from organizations.forms import OrganizationUserAddForm from organizations.forms import OrganizationUserForm from organizations.models import Organization from tests.utils import request_factory_login User = get_user_model() class TestOrganizationAddForm(TestCase): """ Tests for adding new organizations """ def setUp(self): self.factory = RequestFactory() def test_expected_valid_data_validates(self): """Test our happy path""" request = self.factory.request() form = OrganizationAddForm( request, data={"slug": "new_org", "name": "New Org", "email": "cthulu@oldgods.org"}, ) self.assertTrue(form.is_valid()) def test_add_organization_for_existing_user(self): user = User.objects.create_user( "timmy", password="ajsdkfa3", email="timmy@whoa.com" ) request = self.factory.request() form = OrganizationAddForm( request, data={"slug": "new_org", "name": "New Org", "email": user.email} ) self.assertTrue(form.is_valid()) new_org = form.save() self.assertTrue(new_org.is_active) self.assertEqual(new_org.name, "New Org") def test_add_organization_for_new_user(self): user = User.objects.create_user( "timmy", password="ajsdkfa3", email="timmy@whoa.com" ) request = request_factory_login(self.factory, user) form = OrganizationAddForm( request, data={ "slug": "new_org", "name": "New Org", "email": "i.am.new.here@geemail.com", }, ) self.assertTrue(form.is_valid()) new_org = form.save() self.assertFalse(new_org.is_active) # Inactive until confirmation class TestOrganizationUserAddForm(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.factory = RequestFactory() self.org = Organization.objects.get(name="Nirvana") self.owner = self.org.organization_users.get(user__username="kurt") def test_multiple_users_exist(self): User.objects.create_user("asdkjf", password="ajsdkfa", email="bob@bob.com") User.objects.create_user("asdkjf1", password="ajsdkfa3", email="bob@bob.com") request = request_factory_login(self.factory, self.owner.user) form = OrganizationUserAddForm( request=request, organization=self.org, data={"email": "bob@bob.com"}, ) self.assertFalse(form.is_valid()) def test_add_user_already_in_organization(self): admin = self.org.organization_users.get(user__username="krist") request = request_factory_login(self.factory, self.owner.user) form = OrganizationUserAddForm( request=request, organization=self.org, data={"email": admin.user.email}, ) self.assertFalse(form.is_valid()) def test_save_org_user_add_form(self): request = request_factory_login(self.factory, self.owner.user) form = OrganizationUserAddForm( request=request, organization=self.org, data={"email": "test_email@example.com", "is_admin": False}, ) self.assertTrue(form.is_valid()) form.save() @override_settings(USE_TZ=True) class TestOrganizationForm(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.factory = RequestFactory() self.org = Organization.objects.get(name="Nirvana") self.admin = self.org.organization_users.get(user__username="krist") self.owner = self.org.organization_users.get(user__username="kurt") def test_admin_edits_org(self): user = self.admin.user request = request_factory_login(self.factory, user) form = OrganizationForm( request, instance=self.org, data={"name": self.org.name, "slug": self.org.slug, "owner": self.owner.pk}, ) self.assertTrue(form.is_valid()) form = OrganizationForm( request, instance=self.org, data={"name": self.org.name, "slug": self.org.slug, "owner": self.admin.pk}, ) self.assertFalse(form.is_valid()) def test_owner_edits_org(self): user = self.owner.user request = request_factory_login(self.factory, user) form = OrganizationForm( request, instance=self.org, data={"name": self.org.name, "slug": self.org.slug, "owner": self.owner.pk}, ) self.assertTrue(form.is_valid()) form = OrganizationForm( request, instance=self.org, data={"name": self.org.name, "slug": self.org.slug, "owner": self.admin.pk}, ) self.assertTrue(form.is_valid()) form.save() self.assertEqual(self.org.owner.organization_user, self.admin) class TestOrganizationUserForm(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.factory = RequestFactory() self.org = Organization.objects.get(name="Nirvana") self.admin = self.org.organization_users.get(user__username="krist") self.owner = self.org.organization_users.get(user__username="kurt") def test_edit_owner_user(self): form = OrganizationUserForm(instance=self.owner, data={"is_admin": True}) self.assertTrue(form.is_valid()) form = OrganizationUserForm(instance=self.owner, data={"is_admin": False}) self.assertFalse(form.is_valid()) def test_save_org_form(self): request = request_factory_login(self.factory, self.owner.user) form = OrganizationForm( request, instance=self.org, data={"name": self.org.name, "slug": self.org.slug, "owner": self.owner.pk}, ) self.assertTrue(form.is_valid()) form.save() def test_save_user_form(self): form = OrganizationUserForm(instance=self.owner, data={"is_admin": True}) self.assertTrue(form.is_valid()) form.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_migrations.py0000644000175100001770000000051014527662407022211 0ustar00runnerdocker""" Tests that migrations are not missing """ from django.core.management import call_command import pytest @pytest.mark.django_db def test_no_missing_migrations(): """Check no model changes have been made since the last `./manage.py makemigrations`. """ call_command("makemigrations", check=True, dry_run=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_mixins.py0000644000175100001770000001225414527662407021354 0ustar00runnerdockerfrom django.contrib.auth.models import User from django.core.exceptions import PermissionDenied from django.http import HttpResponse from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from organizations.models import Organization from organizations.models import OrganizationUser from organizations.views.mixins import AdminRequiredMixin from organizations.views.mixins import MembershipRequiredMixin from organizations.views.mixins import OrganizationMixin from organizations.views.mixins import OrganizationUserMixin from organizations.views.mixins import OwnerRequiredMixin from tests.utils import request_factory_login class ViewStub: def __init__(self, **kwargs): self.kwargs = kwargs def get_context_data(self, **kwargs): return kwargs def dispatch(self, request, *args, **kwargs): return HttpResponse("Success") class OrgView(OrganizationMixin, ViewStub): """A testing view class""" pass class UserView(OrganizationUserMixin, ViewStub): """A testing view class""" pass @override_settings(USE_TZ=True) class ObjectMixinTests(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.foo = Organization.objects.get(name="Foo Fighters") self.dave = OrganizationUser.objects.get( user__username="dave", organization=self.foo ) def test_get_org_object(self): view = OrgView(organization_pk=self.foo.pk) self.assertEqual(view.get_object(), self.foo) def test_get_user_object(self): view = UserView(organization_pk=self.foo.pk, user_pk=self.dave.pk) self.assertEqual(view.get_object(), self.dave) self.assertEqual(view.get_organization(), self.foo) def test_get_model(self): """Ensure that the method returns the class object""" self.assertEqual(Organization, OrganizationMixin().get_org_model()) self.assertEqual(Organization, OrganizationUserMixin().get_org_model()) self.assertEqual(OrganizationUser, OrganizationUserMixin().get_user_model()) @override_settings(USE_TZ=True) class AccessMixinTests(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.nirvana = Organization.objects.get(name="Nirvana") self.kurt = User.objects.get(username="kurt") self.krist = User.objects.get(username="krist") self.dave = User.objects.get(username="dave") self.dummy = User.objects.create_user( "dummy", email="dummy@example.com", password="test" ) self.factory = RequestFactory() self.kurt_request = request_factory_login(self.factory, self.kurt) self.krist_request = request_factory_login(self.factory, self.krist) self.dave_request = request_factory_login(self.factory, self.dave) self.dummy_request = request_factory_login(self.factory, self.dummy) def test_member_access(self): class MemberView(MembershipRequiredMixin, OrgView): pass self.assertEqual( 200, MemberView() .dispatch(self.kurt_request, organization_pk=self.nirvana.pk) .status_code, ) self.assertEqual( 200, MemberView() .dispatch(self.krist_request, organization_pk=self.nirvana.pk) .status_code, ) self.assertEqual( 200, MemberView() .dispatch(self.dave_request, organization_pk=self.nirvana.pk) .status_code, ) with self.assertRaises(PermissionDenied): MemberView().dispatch(self.dummy_request, organization_pk=self.nirvana.pk) def test_admin_access(self): class AdminView(AdminRequiredMixin, OrgView): pass self.assertEqual( 200, AdminView() .dispatch(self.kurt_request, organization_pk=self.nirvana.pk) .status_code, ) self.assertEqual( 200, AdminView() .dispatch(self.krist_request, organization_pk=self.nirvana.pk) .status_code, ) # Superuser self.assertEqual( 200, AdminView() .dispatch(self.dave_request, organization_pk=self.nirvana.pk) .status_code, ) with self.assertRaises(PermissionDenied): AdminView().dispatch(self.dummy_request, organization_pk=self.nirvana.pk) def test_owner_access(self): class OwnerView(OwnerRequiredMixin, OrgView): pass self.assertEqual( 200, OwnerView() .dispatch(self.kurt_request, organization_pk=self.nirvana.pk) .status_code, ) with self.assertRaises(PermissionDenied): OwnerView().dispatch(self.krist_request, organization_pk=self.nirvana.pk) # Superuser self.assertEqual( 200, OwnerView() .dispatch(self.dave_request, organization_pk=self.nirvana.pk) .status_code, ) with self.assertRaises(PermissionDenied): OwnerView().dispatch(self.dummy_request, organization_pk=self.nirvana.pk) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_models.py0000644000175100001770000001704414527662407021332 0ustar00runnerdockerfrom functools import partial from django.contrib.auth.models import User from django.db import IntegrityError from django.test import TestCase from django.test.utils import override_settings from organizations.models import Organization from organizations.models import OrganizationInvitation from organizations.models import OrganizationOwner from organizations.models import OrganizationUser from organizations.utils import create_organization from test_abstract.models import CustomOrganization from test_accounts.models import Account from test_accounts.models import AccountInvitation from test_custom.models import Team @override_settings(USE_TZ=True) class ActiveManagerTests(TestCase): fixtures = ["users.json", "orgs.json"] def test_active(self): self.assertEqual(3, Organization.objects.all().count()) self.assertEqual(2, Organization.active.all().count()) def test_by_user(self): user = User.objects.get(username="dave") self.assertEqual(3, Organization.objects.get_for_user(user).count()) self.assertEqual(2, Organization.active.get_for_user(user).count()) @override_settings(USE_TZ=True) class OrgModelTests(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.kurt = User.objects.get(username="kurt") self.dave = User.objects.get(username="dave") self.krist = User.objects.get(username="krist") self.duder = User.objects.get(username="duder") self.nirvana = Organization.objects.get(name="Nirvana") self.foo = Organization.objects.get(name="Foo Fighters") def test_invitation_model(self): assert Organization.invitation_model == OrganizationInvitation def test_org_string_representation(self): """Ensure that models' string representation are error free""" self.foo.name = "Föö Fíghterß" self.assertTrue("{0}".format(self.foo)) self.assertTrue("{0}".format(self.foo.owner)) self.assertTrue("{0}".format(self.foo.owner.organization_user)) def test_relation_name(self): """Ensure user-related name is accessible from common attribute""" self.assertEqual(self.foo.user_relation_name, "organizations_organization") def test_duplicate_members(self): """Ensure that a User can only have one OrganizationUser object""" self.assertRaises(IntegrityError, self.nirvana.add_user, self.dave) def test_is_member(self): self.assertTrue(self.nirvana.is_member(self.kurt)) self.assertTrue(self.nirvana.is_member(self.dave)) self.assertTrue(self.foo.is_member(self.dave)) self.assertFalse(self.foo.is_member(self.kurt)) def test_is_admin(self): self.assertTrue(self.nirvana.is_admin(self.kurt)) self.assertTrue(self.nirvana.is_admin(self.krist)) self.assertFalse(self.nirvana.is_admin(self.dave)) self.assertTrue(self.foo.is_admin(self.dave)) def test_is_owner(self): self.assertTrue(self.nirvana.is_owner(self.kurt)) self.assertTrue(self.foo.is_owner(self.dave)) self.assertFalse(self.nirvana.is_owner(self.dave)) self.assertFalse(self.nirvana.is_owner(self.krist)) def test_add_user(self): new_guy = self.foo.add_user(self.krist) self.assertTrue(isinstance(new_guy, OrganizationUser)) self.assertEqual(new_guy.organization, self.foo) def test_remove_user(self): self.foo.add_user(self.krist) self.foo.remove_user(self.krist) self.assertFalse(self.foo.users.filter(pk=self.krist.pk).exists()) def test_get_or_add_user(self): """Ensure `get_or_add_user` adds a user IFF it exists""" new_guy, created = self.foo.get_or_add_user(self.duder) self.assertTrue(isinstance(new_guy, OrganizationUser)) self.assertEqual(new_guy.organization, self.foo) self.assertTrue(created) new_guy, created = self.foo.get_or_add_user(self.dave) self.assertTrue(isinstance(new_guy, OrganizationUser)) self.assertFalse(created) def test_delete_owner(self): from organizations.exceptions import OwnershipRequired owner = self.nirvana.owner.organization_user self.assertRaises(OwnershipRequired, owner.delete) def test_change_owner(self): admin = self.nirvana.organization_users.get(user__username="krist") self.nirvana.change_owner(admin) owner = self.nirvana.owner.organization_user self.assertEqual(owner, admin) def test_delete_missing_owner(self): """Ensure an org user can be deleted when there is no owner""" org = Organization.objects.create(name="Some test", slug="some-test") # Avoid the Organization.add_user method which would make an owner org_user = OrganizationUser.objects.create(user=self.kurt, organization=org) # Just make sure it doesn't raise an error org_user.delete() def test_nonmember_owner(self): from organizations.exceptions import OrganizationMismatch foo_user = self.foo.owner self.nirvana.owner = foo_user self.assertRaises(OrganizationMismatch, self.nirvana.owner.save) @override_settings(USE_TZ=True) class OrgDeleteTests(TestCase): fixtures = ["users.json", "orgs.json"] def test_delete_account(self): """Ensure Users are not deleted on the cascade""" self.assertEqual(3, OrganizationOwner.objects.all().count()) self.assertEqual(4, User.objects.all().count()) scream = Organization.objects.get(name="Scream") scream.delete() self.assertEqual(2, OrganizationOwner.objects.all().count()) self.assertEqual(4, User.objects.all().count()) def test_delete_orguser(self): """Ensure the user is not deleted on the cascade""" krist = User.objects.get(username="krist") org_user = OrganizationUser.objects.filter( organization__name="Nirvana", user=krist ) org_user.delete() self.assertTrue(krist.pk) class CustomModelTests(TestCase): # Load the world as we know it. fixtures = ["users.json", "orgs.json"] def setUp(self): self.kurt = User.objects.get(username="kurt") self.dave = User.objects.get(username="dave") self.krist = User.objects.get(username="krist") self.duder = User.objects.get(username="duder") self.red_account = Account.objects.create( name="Red Account", monthly_subscription=1200 ) def test_invitation_model(self): assert Account.invitation_model == AccountInvitation def test_org_string(self): self.assertEqual(self.red_account.__str__(), "Red Account") def test_relation_name(self): """Ensure user-related name is accessible from common attribute""" self.assertEqual(self.red_account.user_relation_name, "test_accounts_account") def test_change_user(self): """Ensure custom organizations validate in owner change""" create_team = partial(create_organization, model=Team) hometeam = create_team(self.dave, "Hometeam") duder_org_user = hometeam.add_user(self.duder) hometeam.owner.organization_user = duder_org_user hometeam.owner.save() def test_abstract_change_user(self): """ Ensure custom organizations inheriting abstract model validate in owner change """ create_org = partial(create_organization, model=CustomOrganization) org1 = create_org(self.dave, "Org1") duder_org_user = org1.add_user(self.duder) org1.owner.organization_user = duder_org_user org1.owner.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_signals.py0000644000175100001770000000521714527662407021506 0ustar00runnerdockerfrom django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings from mock import call from mock_django.signals import mock_signal_receiver from organizations.models import Organization from organizations.signals import owner_changed from organizations.signals import user_added from organizations.signals import user_removed @override_settings(USE_TZ=True) class SignalsTestCase(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.kurt = User.objects.get(username="kurt") self.dave = User.objects.get(username="dave") self.krist = User.objects.get(username="krist") self.duder = User.objects.get(username="duder") self.foo = Organization.objects.get(name="Foo Fighters") self.org = Organization.objects.get(name="Nirvana") self.admin = self.org.organization_users.get(user__username="krist") self.owner = self.org.organization_users.get(user__username="kurt") def test_user_added_called(self): with mock_signal_receiver(user_added) as add_receiver: self.foo.add_user(self.krist) self.assertEqual( add_receiver.call_args_list, [call(signal=user_added, sender=self.foo, user=self.krist)], ) with mock_signal_receiver(user_added) as add_receiver: self.foo.get_or_add_user(self.duder) self.assertEqual( add_receiver.call_args_list, [call(signal=user_added, sender=self.foo, user=self.duder)], ) def test_user_added_not_called(self): with mock_signal_receiver(user_added) as add_receiver: self.foo.get_or_add_user(self.dave) self.assertEqual(add_receiver.call_args_list, []) def test_user_removed_called(self): with mock_signal_receiver(user_removed) as remove_receiver: self.foo.add_user(self.krist) self.foo.remove_user(self.krist) self.assertEqual( remove_receiver.call_args_list, [call(signal=user_removed, sender=self.foo, user=self.krist)], ) def test_owner_changed_called(self): with mock_signal_receiver(owner_changed) as changed_receiver: self.org.change_owner(self.admin) self.assertEqual( changed_receiver.call_args_list, [ call( signal=owner_changed, sender=self.org, old=self.owner, new=self.admin, ) ], ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_templatetags.py0000644000175100001770000000374514527662407022544 0ustar00runnerdockerfrom django.contrib.auth.models import User from django.template import Context from django.template import Template from django.test import TestCase from django.test.utils import override_settings from organizations.models import Organization @override_settings(USE_TZ=True) class TestTagsAndFilters(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.kurt = User.objects.get(username="kurt") self.dave = User.objects.get(username="dave") self.nirvana = Organization.objects.get(name="Nirvana") self.foo = Organization.objects.get(name="Foo Fighters") self.context = {} def test_organization_users_tag(self): self.context = {"organization": self.nirvana} out = Template( "{% load org_tags %}" "{% organization_users organization %}" ).render(Context(self.context)) self.assertIn("Kurt", out) self.assertIn("Dave", out) def test_is_owner_org_filter(self): self.context = {"organization": self.nirvana, "user": self.kurt} out = Template( "{% load org_tags %}" "{% if organization|is_owner:user %}" "Is Owner" "{% endif %}" ).render(Context(self.context)) self.assertEqual(out, "Is Owner") def test_is_admin_org_filter(self): self.context = {"organization": self.foo, "user": self.dave} out = Template( "{% load org_tags %}" "{% if organization|is_admin:user %}" "Is Admin" "{% endif %}" ).render(Context(self.context)) self.assertEqual(out, "Is Admin") def test_is_not_admin_org_filter(self): self.context = {"organization": self.nirvana, "user": self.dave} out = Template( "{% load org_tags %}" "{% if not organization|is_admin:user %}" "Is Not Admin" "{% endif %}" ).render(Context(self.context)) self.assertEqual(out, "Is Not Admin") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_utils.py0000644000175100001770000000570014527662407021203 0ustar00runnerdockerfrom functools import partial from django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings from organizations.models import Organization from organizations.utils import create_organization from organizations.utils import model_field_attr from test_abstract.models import CustomOrganization from test_accounts.models import Account @override_settings(USE_TZ=True) class CreateOrgTests(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.user = User.objects.get(username="dave") def test_create_organization(self): acme = create_organization( self.user, "Acme", org_defaults={"slug": "acme-slug"} ) self.assertTrue(isinstance(acme, Organization)) self.assertEqual(self.user, acme.owner.organization_user.user) self.assertTrue(acme.owner.organization_user.is_admin) def test_create_custom_org(self): custom = create_organization(self.user, "Custom", model=Account) self.assertTrue(isinstance(custom, Account)) self.assertEqual(self.user, custom.owner.organization_user.user) def test_create_custom_org_from_abstract(self): custom = create_organization(self.user, "Custom", model=CustomOrganization) self.assertTrue(isinstance(custom, CustomOrganization)) self.assertEqual(self.user, custom.owner.organization_user.user) def test_defaults(self): """Ensure models are created with defaults as specified""" # Default models org = create_organization( self.user, "Is Admin", org_defaults={"slug": "is-admin-212", "is_active": False}, org_user_defaults={"is_admin": False}, ) self.assertFalse(org.is_active) self.assertFalse(org.owner.organization_user.is_admin) # Custom models create_account = partial( create_organization, model=Account, org_defaults={"monthly_subscription": 99}, org_user_defaults={"user_type": "B"}, ) myaccount = create_account(self.user, name="My New Account") self.assertEqual(myaccount.monthly_subscription, 99) def test_backwards_compat(self): """Ensure old optional arguments still work""" org = create_organization(self.user, "Is Admin", "my-slug", is_active=False) self.assertFalse(org.is_active) custom = create_organization(self.user, "Custom org", org_model=Account) self.assertTrue(isinstance(custom, Account)) class AttributeUtilTests(TestCase): def test_present_field(self): self.assertTrue(model_field_attr(User, "username", "max_length")) def test_absent_field(self): self.assertRaises(KeyError, model_field_attr, User, "blahblah", "max_length") def test_absent_attr(self): self.assertRaises( AttributeError, model_field_attr, User, "username", "mariopoints" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700750599.0 django-organizations-2.3.1/tests/test_views.py0000644000175100001770000002534414527662407021206 0ustar00runnerdockerfrom django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ObjectDoesNotExist from django.http import Http404 from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from django.urls import reverse import pytest from organizations.models import Organization from organizations.models import OrganizationUser from organizations.utils import create_organization from organizations.views import base from test_accounts.models import Account from test_accounts.models import AccountUser from tests.utils import request_factory_login @pytest.fixture def account_user(): yield User.objects.create(username="AccountUser", email="akjdkj@kjdk.com") @pytest.fixture def account_account(account_user): yield create_organization(account_user, "Acme", org_model=Account) @pytest.fixture def org_organization(account_user): yield create_organization(account_user, "Acme", org_model=Organization) @pytest.fixture def extra_org_user(org_organization): new_user = User.objects.create(username="NotYou", email="not@you.com") org_organization.add_user(new_user) yield new_user @pytest.fixture def invitee_user(): yield User.objects.create_user( "newmember", email="jd@123.com", password="password123" ) class TestUserReminderView: @pytest.mark.parametrize("method", [("get"), ("post")]) def test_bad_request_for_active_user( self, rf, account_account, account_user, invitee_user, method ): class OrgUserReminderView(base.BaseOrganizationUserRemind): org_model = Account user_model = AccountUser request = getattr(rf, method)("/", user=account_user) kwargs = {"organization_pk": account_account.pk, "user_pk": account_user.pk} response = OrgUserReminderView.as_view()(request, **kwargs) assert response.status_code == 410 def test_organization_user_reminder( self, rf, account_account, account_user, invitee_user ): class OrgUserReminderView(base.BaseOrganizationUserRemind): org_model = Account user_model = AccountUser invitee_user.is_active = False invitee_user.save() account_account.add_user(invitee_user) request = rf.get("/", user=account_user) kwargs = {"organization_pk": account_account.pk, "user_pk": invitee_user.pk} response = OrgUserReminderView.as_view()(request, **kwargs) assert response.status_code == 200 request = rf.post("/") request.user = account_user response = OrgUserReminderView.as_view()(request, **kwargs) assert response.status_code == 302 class TestSignupView: def test_anon_user_can_access_signup_view(self, rf): """""" request = request_factory_login(rf) assert base.OrganizationSignup.as_view()(request).status_code == 200 def test_authenticated_user_is_redirected_from_signup_view(self, rf, account_user): request = request_factory_login(rf, account_user) assert base.OrganizationSignup.as_view()(request).status_code == 302 def test_anon_user_signup_base_class_has_no_success_url(self, rf): """""" request = request_factory_login( rf, method="post", path="/", data={ "name": "An Association of Very Interesting People", "slug": "people", "email": "hey@people.org", }, ) with pytest.raises(ImproperlyConfigured): base.OrganizationSignup.as_view()(request) def test_anon_user_can_signup(self, rf): """""" class SignupView(base.OrganizationSignup): success_url = "/" request = request_factory_login( rf, method="post", path="/", data={ "name": "An Association of Very Interesting People", "slug": "people", "email": "hey@people.org", }, ) response = SignupView.as_view()(request) assert response.status_code == 302 # Verify its in the database Organization.objects.get(slug="an-association-of-very-interesting-people", is_active=False) class TestBaseCreateOrganization: def test_get_org_create_view(self, rf): request = request_factory_login(rf) assert base.BaseOrganizationCreate.as_view()(request).status_code == 200 def test_create_new_org(self, rf, account_user): request = request_factory_login( rf, user=account_user, method="post", path="/", data={"name": "Vizsla Club", "slug": "vizsla", "email": "hey@woof.org"}, ) response = base.BaseOrganizationCreate.as_view()(request) assert response.status_code == 302 assert response["Location"] == reverse("organization_list") assert Organization.objects.get(slug="vizsla-club") class TestBaseOrganizationDelete: def test_get_org_delete(self, rf, org_organization, account_user): request = request_factory_login(rf, user=account_user) kwargs = {"organization_pk": org_organization.pk} response = base.BaseOrganizationDelete.as_view()(request, **kwargs) assert response.status_code == 200 def test_delete_organization_with_post(self, rf, org_organization, account_user): request = request_factory_login(rf, user=account_user, method="post", path="/") kwargs = {"organization_pk": org_organization.pk} response = base.BaseOrganizationDelete.as_view()(request, **kwargs) assert response.status_code == 302 with pytest.raises(ObjectDoesNotExist): org_organization.refresh_from_db() class TestBaseOrganizationUserDelete: def test_get_org_user_delete( self, rf, org_organization, account_user, extra_org_user ): request = request_factory_login(rf, user=account_user) kwargs = {"organization_pk": org_organization.pk, "user_pk": extra_org_user.pk} response = base.BaseOrganizationUserDelete.as_view()(request, **kwargs) assert response.status_code == 200 def test_delete_organization_user_with_post( self, rf, org_organization, account_user, extra_org_user ): request = request_factory_login(rf, user=account_user, method="post", path="/") kwargs = {"organization_pk": org_organization.pk, "user_pk": extra_org_user.pk} response = base.BaseOrganizationUserDelete.as_view()(request, **kwargs) assert response.status_code == 302 with pytest.raises(ObjectDoesNotExist): OrganizationUser.objects.get(user=extra_org_user) @override_settings(USE_TZ=True) class TestBasicOrgViews(TestCase): fixtures = ["users.json", "orgs.json"] def setUp(self): self.kurt = User.objects.get(username="kurt") self.dave = User.objects.get(username="dave") self.dummy = User.objects.create_user( "dummy", email="dummy@example.com", password="test" ) self.nirvana = Organization.objects.get(name="Nirvana") self.factory = RequestFactory() self.kurt_request = request_factory_login(self.factory, self.kurt) self.dave_request = request_factory_login(self.factory, self.dave) self.anon_request = request_factory_login(self.factory) def test_org_list(self): """Ensure that the status code 200 is returned""" self.assertEqual( 200, base.BaseOrganizationList(request=self.kurt_request) .get(self.kurt_request) .status_code, ) self.assertEqual( 200, base.BaseOrganizationList(request=self.dave_request) .get(self.dave_request) .status_code, ) def test_org_list_queryset(self): """Ensure only active organizations belonging to the user are listed""" self.assertEqual( 1, base.BaseOrganizationList(request=self.kurt_request).get_queryset().count(), ) self.assertEqual( 2, base.BaseOrganizationList(request=self.dave_request).get_queryset().count(), ) def test_org_detail(self): kwargs = {"organization_pk": self.nirvana.pk} self.assertEqual( 200, base.BaseOrganizationDetail(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, ) def test_org_update(self): kwargs = {"organization_pk": self.nirvana.pk} self.assertEqual( 200, base.BaseOrganizationUpdate(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, ) def test_user_list(self): kwargs = {"organization_pk": self.nirvana.pk} self.assertEqual( 200, base.BaseOrganizationUserList(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, ) def test_user_detail(self): kwargs = {"organization_pk": self.nirvana.pk, "user_pk": self.kurt.pk} self.assertEqual( 200, base.BaseOrganizationUserDetail(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, ) def test_bad_user_detail(self): kwargs = {"organization_pk": self.nirvana.pk, "user_pk": self.dummy.pk} self.assertRaises( Http404, base.BaseOrganizationUserDetail( request=self.kurt_request, kwargs=kwargs ).get, self.kurt_request, **kwargs ) def test_user_create_get(self): kwargs = {"organization_pk": self.nirvana.pk} self.assertEqual( 200, base.BaseOrganizationUserCreate(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, ) def test_user_create_post(self): request = request_factory_login( self.factory, self.kurt, path="/", method="post", data={"email": "roadie@yahoo.com"}, ) kwargs = {"organization_pk": self.nirvana.pk} self.assertEqual( 302, base.BaseOrganizationUserCreate.as_view()(request, **kwargs).status_code, ) def test_user_update(self): kwargs = {"organization_pk": self.nirvana.pk, "user_pk": self.kurt.pk} self.assertEqual( 200, base.BaseOrganizationUserUpdate(request=self.kurt_request, kwargs=kwargs) .get(self.kurt_request, **kwargs) .status_code, )