././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9598565 django-organizations-2.0.1/0000755000076500000240000000000000000000000017120 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327231.0 django-organizations-2.0.1/AUTHORS.rst0000644000076500000240000000224000000000000020775 0ustar00benlopatinstaff00000000000000Primary 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 `_ If your name is missing as a contributor that's my oversight, let me know at ben@benlopatin.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327160.0 django-organizations-2.0.1/HISTORY.rst0000644000076500000240000001256000000000000021017 0ustar00benlopatinstaff00000000000000.. :changelog: History ======= 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/LICENSE0000644000076500000240000000243200000000000020126 0ustar00benlopatinstaff00000000000000Copyright (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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/MANIFEST.in0000644000076500000240000000023600000000000020657 0ustar00benlopatinstaff00000000000000include setup.py include setup.cfg include README.rst include MANIFEST.in include HISTORY.rst include LICENSE recursive-include src/organizations/templates * ././@PaxHeader0000000000000000000000000000003200000000000011450 xustar000000000000000026 mtime=1632327379.96006 django-organizations-2.0.1/PKG-INFO0000644000076500000240000005010700000000000020220 0ustar00benlopatinstaff00000000000000Metadata-Version: 1.0 Name: django-organizations Version: 2.0.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 Description: ==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |travis| |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 .. |travis| image:: https://travis-ci.org/bennylope/django-organizations.svg?branch=master :alt: Travis-CI Build Status :target: https://travis-ci.org/bennylope/django-organizations .. |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 2.2.x against Python 3.5, 3.6, 3.7, 3.8 * Django 3.0.x against Python 3.6, 3.7, 3.8 * Django 3.1.x against Python 3.6, 3.7, 3.8 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 `Travis CI `_. 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 satisifed 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 .. :changelog: History ======= 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 Platform: OS Independent ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/README.rst0000644000076500000240000002405400000000000020614 0ustar00benlopatinstaff00000000000000==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |travis| |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 .. |travis| image:: https://travis-ci.org/bennylope/django-organizations.svg?branch=master :alt: Travis-CI Build Status :target: https://travis-ci.org/bennylope/django-organizations .. |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 2.2.x against Python 3.5, 3.6, 3.7, 3.8 * Django 3.0.x against Python 3.6, 3.7, 3.8 * Django 3.1.x against Python 3.6, 3.7, 3.8 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 `Travis CI `_. 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 satisifed 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 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1632327379.960552 django-organizations-2.0.1/setup.cfg0000644000076500000240000000215400000000000020743 0ustar00benlopatinstaff00000000000000[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, HISTORY.rst license = BSD License platforms = OS Independent [options] zip_safe = False include_package_data = True packages = find: package_dir = =src install_requires = six Django>=2.2.0 typing; python_version<"3.6" 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.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Development Status :: 5 - Production/Stable [options.packages.find] where = src [bdist_wheel] universal = 1 [build-system] requires = setuptools >= "40.9.0" wheel [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/setup.py0000644000076500000240000000015300000000000020631 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- """ See setup.cfg for packaging settings """ from setuptools import setup setup() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9413476 django-organizations-2.0.1/src/0000755000076500000240000000000000000000000017707 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9460936 django-organizations-2.0.1/src/django_organizations.egg-info/0000755000076500000240000000000000000000000025612 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327379.0 django-organizations-2.0.1/src/django_organizations.egg-info/PKG-INFO0000644000076500000240000005010700000000000026712 0ustar00benlopatinstaff00000000000000Metadata-Version: 1.0 Name: django-organizations Version: 2.0.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 Description: ==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com) * - Status - |docs| |travis| |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 .. |travis| image:: https://travis-ci.org/bennylope/django-organizations.svg?branch=master :alt: Travis-CI Build Status :target: https://travis-ci.org/bennylope/django-organizations .. |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 2.2.x against Python 3.5, 3.6, 3.7, 3.8 * Django 3.0.x against Python 3.6, 3.7, 3.8 * Django 3.1.x against Python 3.6, 3.7, 3.8 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 `Travis CI `_. 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 satisifed 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 .. :changelog: History ======= 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 Platform: OS Independent ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327379.0 django-organizations-2.0.1/src/django_organizations.egg-info/SOURCES.txt0000644000076500000240000000607000000000000027501 0ustar00benlopatinstaff00000000000000AUTHORS.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/__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././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327379.0 django-organizations-2.0.1/src/django_organizations.egg-info/dependency_links.txt0000644000076500000240000000000100000000000031660 0ustar00benlopatinstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1606424230.0 django-organizations-2.0.1/src/django_organizations.egg-info/not-zip-safe0000644000076500000240000000000100000000000030040 0ustar00benlopatinstaff00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327379.0 django-organizations-2.0.1/src/django_organizations.egg-info/requires.txt0000644000076500000240000000006400000000000030212 0ustar00benlopatinstaff00000000000000six Django>=2.2.0 [:python_version < "3.6"] typing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327379.0 django-organizations-2.0.1/src/django_organizations.egg-info/top_level.txt0000644000076500000240000000001600000000000030341 0ustar00benlopatinstaff00000000000000organizations ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9496996 django-organizations-2.0.1/src/organizations/0000755000076500000240000000000000000000000022576 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327151.0 django-organizations-2.0.1/src/organizations/__init__.py0000644000076500000240000000030100000000000024701 0ustar00benlopatinstaff00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- __author__ = "Ben Lopatin" __email__ = "ben@benlopatin.com" __version__ = "2.0.1" default_app_config = "organizations.apps.OrganizationsConfig" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/abstract.py0000644000076500000240000002044600000000000024761 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 _ import six from organizations.base import AbstractBaseInvitation from organizations.base import AbstractBaseOrganization from organizations.base import AbstractBaseOrganizationOwner from organizations.base import AbstractBaseOrganizationUser 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( six.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( six.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( six.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( six.with_metaclass(OrgMeta, SharedBaseModel, AbstractBaseInvitation) ): """ Abstract OrganizationInvitationBase model """ class Meta: abstract = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/admin.py0000644000076500000240000000152300000000000024241 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/app_settings.py0000644000076500000240000000074600000000000025657 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/apps.py0000644000076500000240000000032700000000000024115 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- from django.apps import AppConfig class OrganizationsConfig(AppConfig): name = "organizations" verbose_name = "Organizations" default_auto_field = 'django.db.models.AutoField' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9506066 django-organizations-2.0.1/src/organizations/backends/0000755000076500000240000000000000000000000024350 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/backends/__init__.py0000644000076500000240000000261400000000000026464 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/backends/defaults.py0000644000076500000240000003171000000000000026533 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- """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"], ) 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/backends/forms.py0000644000076500000240000000175600000000000026061 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/backends/modeled.py0000644000076500000240000001554200000000000026342 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- """ 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/base.py0000644000076500000240000003064600000000000024073 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 _ import six from organizations import signals from organizations.managers import ActiveOrgManager from organizations.managers import OrgManager USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User") class UnicodeMixin: """ Python 2 and 3 string representation support. Legacy cruft. Removing entirely even from migrations affects the meta class creation. """ 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 six.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(six.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(six.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(six.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(six.with_metaclass(OrgMeta, AbstractBaseInvitation)): class Meta(AbstractBaseInvitation.Meta): abstract = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/base_admin.py0000644000076500000240000000114300000000000025231 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/exceptions.py0000644000076500000240000000055000000000000025331 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/fields.py0000644000076500000240000000601000000000000024413 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- """ 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""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/forms.py0000644000076500000240000001426500000000000024306 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/managers.py0000644000076500000240000000066500000000000024754 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9517355 django-organizations-2.0.1/src/organizations/migrations/0000755000076500000240000000000000000000000024752 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/migrations/0001_initial.py0000644000076500000240000001422000000000000027414 0ustar00benlopatinstaff00000000000000# 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, }, bases=(organizations.base.UnicodeMixin, models.Model), ), 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, }, bases=(organizations.base.UnicodeMixin, models.Model), ), 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, }, bases=(organizations.base.UnicodeMixin, models.Model), ), 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")} ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/migrations/0002_model_update.py0000644000076500000240000000116500000000000030432 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- from 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, ), ) ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/migrations/0003_field_fix_and_editable.py0000644000076500000240000000113200000000000032367 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- from 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, ), ) ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/migrations/0004_organizationinvitation.py0000644000076500000240000000553100000000000032604 0ustar00benlopatinstaff00000000000000# 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}, bases=(organizations.base.UnicodeMixin, models.Model), ) ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/migrations/__init__.py0000644000076500000240000000000000000000000027051 0ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/models.py0000644000076500000240000000164100000000000024435 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/signals.py0000644000076500000240000000032700000000000024612 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- import django.dispatch user_added = django.dispatch.Signal() user_removed = django.dispatch.Signal() invitation_accepted = django.dispatch.Signal() owner_changed = django.dispatch.Signal() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9520187 django-organizations-2.0.1/src/organizations/templates/0000755000076500000240000000000000000000000024574 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1632327379.955746 django-organizations-2.0.1/src/organizations/templates/organizations/0000755000076500000240000000000000000000000027463 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9581652 django-organizations-2.0.1/src/organizations/templates/organizations/email/0000755000076500000240000000000000000000000030552 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/activation_body.html0000644000076500000240000000000000000000000034604 0ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/activation_subject.txt0000644000076500000240000000000000000000000035161 0ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/invitation_body.html0000644000076500000240000000047500000000000034647 0ustar00benlopatinstaff00000000000000You'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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/invitation_subject.txt0000644000076500000240000000006600000000000035220 0ustar00benlopatinstaff00000000000000{% spaceless %}You've been invited!{% endspaceless %} ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=django-organizations-2.0.1/src/organizations/templates/organizations/email/modeled_invitation_body.html 22 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/modeled_invitation_body.h0000644000076500000240000000043700000000000035621 0ustar00benlopatinstaff00000000000000You'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. ././@PaxHeader0000000000000000000000000000021100000000000011447 xustar0000000000000000115 path=django-organizations-2.0.1/src/organizations/templates/organizations/email/modeled_invitation_subject.txt 22 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/modeled_invitation_subjec0000644000076500000240000000006600000000000035707 0ustar00benlopatinstaff00000000000000{% spaceless %}You've been invited!{% endspaceless %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/notification_body.html0000644000076500000240000000045700000000000035151 0ustar00benlopatinstaff00000000000000You'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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/notification_subject.txt0000644000076500000240000000010600000000000035515 0ustar00benlopatinstaff00000000000000{% spaceless %}You've been added to an organization{% endspaceless %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/reminder_body.html0000644000076500000240000000047500000000000034270 0ustar00benlopatinstaff00000000000000You'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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/email/reminder_subject.txt0000644000076500000240000000002000000000000034627 0ustar00benlopatinstaff00000000000000Just a reminder ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/invitation_join.html0000644000076500000240000000024100000000000033551 0ustar00benlopatinstaff00000000000000{% load i18n %} {% trans "Would you like to join?" %}
{% csrf_token %}
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/login.html0000644000076500000240000000065700000000000031471 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

Log in to your dashboard

{% csrf_token %} {{ form }}

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

{% endblock %} ././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=django-organizations-2.0.1/src/organizations/templates/organizations/organization_confirm_delete.html 22 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organization_confirm_delete.htm0000644000076500000240000000046100000000000035741 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

{{ organization }}

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

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organization_detail.html0000644000076500000240000000073000000000000034377 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load i18n %} {% load org_tags %} {% block content %}

{{ organization }}

{% organization_users organization %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organization_form.html0000644000076500000240000000032700000000000034102 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organization_list.html0000644000076500000240000000043300000000000034110 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load i18n %} {% block content %}

{% trans "organizations" %}

{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organization_users.html0000644000076500000240000000043500000000000034300 0ustar00benlopatinstaff00000000000000{% load i18n %}
    {% for organization_user in organization_users %}
  • {{ organization_user }} {% if not organization_user.user.is_active %}{% trans "Send reminder" %}{% endif %}
  • {% endfor %}
././@PaxHeader0000000000000000000000000000021100000000000011447 xustar0000000000000000115 path=django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_confirm_delete.html 22 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_confirm_delete0000644000076500000240000000032700000000000036052 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_detail.html0000644000076500000240000000115300000000000035276 0ustar00benlopatinstaff00000000000000{% 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 %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_form.html0000644000076500000240000000064200000000000035001 0ustar00benlopatinstaff00000000000000{% 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 %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_list.html0000644000076500000240000000045300000000000035011 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load i18n %} {% load org_tags %} {% block content %}

{{ organization }}'s Members

{% organization_users organization %} {% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/organizationuser_remind.html0000644000076500000240000000033600000000000035314 0ustar00benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/register_form.html0000644000076500000240000000021600000000000033217 0ustar00benlopatinstaff00000000000000
{% csrf_token %} {{ form }}
././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations/register_success.html0000644000076500000240000000004600000000000033725 0ustar00benlopatinstaff00000000000000{% load i18n %} {% trans "Thanks!" %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1621113397.0 django-organizations-2.0.1/src/organizations/templates/organizations_base.html0000644000076500000240000000004200000000000031337 0ustar00benlopatinstaff00000000000000{% block content %}{% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1632327379.9586675 django-organizations-2.0.1/src/organizations/templatetags/0000755000076500000240000000000000000000000025270 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/templatetags/__init__.py0000644000076500000240000000003000000000000027372 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/templatetags/org_tags.py0000644000076500000240000000072700000000000027455 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/urls.py0000644000076500000240000000651700000000000024146 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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", ), ] ), ), ] ), ), ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/utils.py0000644000076500000240000000515300000000000024314 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1632327379.959568 django-organizations-2.0.1/src/organizations/views/0000755000076500000240000000000000000000000023733 5ustar00benlopatinstaff00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/views/__init__.py0000644000076500000240000000003000000000000026035 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/views/base.py0000644000076500000240000002104600000000000025222 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/views/default.py0000644000076500000240000000245400000000000025736 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1632327105.0 django-organizations-2.0.1/src/organizations/views/mixins.py0000644000076500000240000000740100000000000025616 0ustar00benlopatinstaff00000000000000# -*- coding: utf-8 -*- 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)