django-organizations-1.0.0/0000755000076500000240000000000013214100244017104 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/PKG-INFO0000644000076500000240000004455013214100244020211 0ustar benlopatinstaff00000000000000Metadata-Version: 1.1 Name: django-organizations Version: 1.0.0 Summary: Group accounts for Django Home-page: https://github.com/bennylope/django-organizations/ Author: Ben Lopatin Author-email: ben@wellfire.co 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 / https://wellfire.co) * - 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 `_ Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations 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:: 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:: 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:: ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative (note: this is not compatible with Django 2.0):: 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 `_ Note that as of django-autoslug 1.9.3. it is incompatible with Django 2.x 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:: 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:: >>> 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. Development & Contributing ========================== 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 * Python 3 readiness 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 1.11.x against Python 2.7, 3.4, 3.5, 3.6, and PyPy * Django 2.0.x against Python 3.4, 3.5, 3.6 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. License ======= Anyone is free to use or modify this software under the terms of the BSD license. History ======= 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) 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 Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: Django django-organizations-1.0.0/organizations/0000755000076500000240000000000013214100244021773 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/abstract.py0000644000076500000240000002261113214100213024146 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. import warnings from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ from organizations.base import AbstractBaseOrganization from organizations.base import AbstractBaseOrganizationOwner from organizations.base import AbstractBaseOrganizationUser from organizations.base import OrgMeta from organizations.compat import reverse from organizations.compat import six 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 __unicode__(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 __unicode__(self): return u"{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.id == self.id: 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(AbstractBaseOrganizationUser, self).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 inheritence 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(AbstractBaseOrganizationOwner, self).save(*args, **kwargs) django-organizations-1.0.0/organizations/templatetags/0000755000076500000240000000000013214100244024465 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templatetags/org_tags.py0000644000076500000240000000343212527730133026662 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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 django-organizations-1.0.0/organizations/templatetags/__init__.py0000644000076500000240000000253312527730133026615 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. django-organizations-1.0.0/organizations/signals.py0000644000076500000240000000316112527730133024022 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. import django.dispatch user_kwargs = {"providing_args": ["user"]} user_added = django.dispatch.Signal(**user_kwargs) user_removed = django.dispatch.Signal(**user_kwargs) owner_kwargs = {"providing_args": ["old", "new"]} owner_changed = django.dispatch.Signal(**owner_kwargs) django-organizations-1.0.0/organizations/migrations/0000755000076500000240000000000013214100244024147 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/migrations/__init__.py0000644000076500000240000000000012677351322026267 0ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/migrations/0002_model_update.py0000644000076500000240000000110513214100213027615 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models 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), ), ] django-organizations-1.0.0/organizations/migrations/0001_initial.py0000644000076500000240000001023213214100213026604 0ustar benlopatinstaff00000000000000# Generated by Django 2.0 on 2017-12-05 00:17 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone 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')}, ), ] django-organizations-1.0.0/organizations/mixins.py0000644000076500000240000001227613214100213023660 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext_lazy as _ from organizations.models import Organization from organizations.models import OrganizationUser class OrganizationMixin(object): """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.get_organization()}) return super(OrganizationMixin, self).get_context_data(**kwargs) def get_object(self): if hasattr(self, 'organization'): return self.organization organization_pk = self.kwargs.get('organization_pk', None) self.organization = get_object_or_404(self.get_org_model(), pk=organization_pk) 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(OrganizationUserMixin, self).get_context_data(**kwargs) kwargs.update({self.org_user_context_name: self.object, self.org_context_name: self.object.organization}) return kwargs def get_object(self): """ Returns the OrganizationUser object based on the primary keys for both the organization and the organization user. """ if hasattr(self, 'organization_user'): return self.organization_user organization_pk = self.kwargs.get('organization_pk', None) user_pk = self.kwargs.get('user_pk', None) self.organization_user = get_object_or_404( self.get_user_model().objects.select_related(), user__pk=user_pk, organization__pk=organization_pk) return self.organization_user class MembershipRequiredMixin(object): """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.organization = self.get_organization() if not self.organization.is_member(request.user) and not \ request.user.is_superuser: raise PermissionDenied(_("Wrong organization")) return super(MembershipRequiredMixin, self).dispatch(request, *args, **kwargs) class AdminRequiredMixin(object): """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.organization = self.get_organization() if not self.organization.is_admin(request.user) and not \ request.user.is_superuser: raise PermissionDenied(_("Sorry, admins only")) return super(AdminRequiredMixin, self).dispatch(request, *args, **kwargs) class OwnerRequiredMixin(object): """This mixin presumes that authentication has already been checked""" def dispatch(self, request, *args, **kwargs): self.request = request self.args = args self.kwargs = kwargs self.organization = self.get_organization() 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(OwnerRequiredMixin, self).dispatch(request, *args, **kwargs) django-organizations-1.0.0/organizations/app_settings.py0000644000076500000240000000344513214100213025047 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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') django-organizations-1.0.0/organizations/compat.py0000644000076500000240000000033113214100213023621 0ustar benlopatinstaff00000000000000 try: from django.urls import reverse # noqa except ImportError: from django.core.urlresolvers import reverse # noqa try: import six # noqa except ImportError: from django.utils import six # noqa django-organizations-1.0.0/organizations/models.py0000644000076500000240000000402113214100213023621 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from organizations.abstract import AbstractOrganization 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 django-organizations-1.0.0/organizations/fields.py0000644000076500000240000000610612700335500023622 0ustar benlopatinstaff00000000000000# -*- 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 __future__ import unicode_literals 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(AutoCreatedField, self).__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): 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""" django-organizations-1.0.0/organizations/backends/0000755000076500000240000000000013214100244023545 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/backends/__init__.py0000644000076500000240000000357713214100213025666 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from importlib import import_module from organizations.app_settings import ORGS_INVITATION_BACKEND from organizations.app_settings import ORGS_REGISTRATION_BACKEND def invitation_backend(): # TODO exception handling class_module, class_name = ORGS_INVITATION_BACKEND.rsplit('.', 1) mod = import_module(class_module) return getattr(mod, class_name)() def registration_backend(): class_module, class_name = ORGS_REGISTRATION_BACKEND.rsplit('.', 1) mod = import_module(class_module) return getattr(mod, class_name)() django-organizations-1.0.0/organizations/backends/tokens.py0000644000076500000240000000554212527730133025444 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.conf import settings from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.utils.crypto import constant_time_compare from django.utils.http import base36_to_int REGISTRATION_TIMEOUT_DAYS = getattr(settings, 'REGISTRATION_TIMEOUT_DAYS', 15) class RegistrationTokenGenerator(PasswordResetTokenGenerator): """ Very similar to the password reset token generator, but should allow slightly greater time for timeout, so it only updates one method, replacing PASSWORD_RESET_TIMEOUT_DAYS from the global settings with REGISTRATION_TIMEOUT_DAYS from application settings. Has the additional interface method: -- make_token(user): Returns a token that can be used once to do a password reset for the given user. """ def check_token(self, user, token): """ Check that a password reset token is correct for a given user. """ # Parse the token try: ts_b36, hash = token.split("-") except ValueError: return False try: ts = base36_to_int(ts_b36) except ValueError: return False # Check that the timestamp/uid has not been tampered with if not constant_time_compare(self._make_token_with_timestamp(user, ts), token): return False # Check the timestamp is within limit if (self._num_days(self._today()) - ts) > REGISTRATION_TIMEOUT_DAYS: return False return True django-organizations-1.0.0/organizations/backends/forms.py0000644000076500000240000000632713214100213025251 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django import forms from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ class UserRegistrationForm(forms.ModelForm): """ 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 """ first_name = forms.CharField(max_length=30) last_name = forms.CharField(max_length=30) password = forms.CharField(max_length=30, widget=forms.PasswordInput) password_confirm = forms.CharField(max_length=30, widget=forms.PasswordInput) def __init__(self, *args, **kwargs): super(UserRegistrationForm, self).__init__(*args, **kwargs) self.initial['username'] = '' def clean(self): password = self.cleaned_data.get("password") password_confirm = self.cleaned_data.get("password_confirm") if password != password_confirm or not password: raise forms.ValidationError(_("Your password entries must match")) return super(UserRegistrationForm, self).clean() class Meta: model = get_user_model() exclude = ('is_staff', 'is_superuser', 'is_active', 'last_login', 'date_joined', 'groups', 'user_permissions') 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(OrganizationRegistrationForm, self).save(*args, **kwargs) return OrganizationRegistrationForm django-organizations-1.0.0/organizations/backends/defaults.py0000644000076500000240000003250013214100213025722 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. """Backend classes should provide common interface """ import email.utils import inspect import uuid from django.conf import settings from django.conf.urls import url from django.contrib.auth import authenticate from django.contrib.auth import get_user_model from django.contrib.auth import login 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.utils.translation import ugettext as _ from organizations.backends.forms import UserRegistrationForm from organizations.backends.forms import org_registration_form from organizations.backends.tokens import RegistrationTokenGenerator from organizations.compat import reverse from organizations.utils import create_organization from organizations.utils import default_org_model from organizations.utils import model_field_attr class BaseBackend(object): """ 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): self.user_model = get_user_model() self.org_model = org_model or default_org_model() 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 hasattr(self, 'form_class'): 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 RegistrationTokenGenerator().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 RegistrationTokenGenerator().check_token(user, token): raise Http404(_("Your URL may have expired.")) form = self.get_form(data=request.POST or None, instance=user) if form.is_valid(): form.instance.is_active = True user = form.save() user.set_password(form.cleaned_data['password']) user.save() self.activate_organizations(user) user = authenticate(username=form.cleaned_data['username'], password=form.cleaned_data['password']) 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 = RegistrationTokenGenerator().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 overriden. For instance, to send an HTML message, use the EmailMultiAlternatives message_class and attach the additional conent. """ if sender: 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) 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 [ url(r'^complete/$', view=self.success_view, name="registration_success"), url(r'^(?P[\d]+)-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', view=self.activate_view, name="registration_register"), url(r'^$', view=self.create_view, name="registration_create"), ] 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 """ try: if request.user.is_authenticated(): return redirect("organization_add") except TypeError: 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 [ url(r'^(?P[\d]+)-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', 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.getargspec(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 django-organizations-1.0.0/organizations/base_admin.py0000644000076500000240000000364613051627536024461 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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') django-organizations-1.0.0/organizations/__init__.py0000644000076500000240000000300113214100213024072 0ustar benlopatinstaff00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. __author__ = 'Ben Lopatin' __email__ = 'ben@wellfire.co' __version__ = '1.0.0' default_app_config = 'organizations.apps.OrganizationsConfig' django-organizations-1.0.0/organizations/apps.py0000644000076500000240000000274412527730133023333 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.apps import AppConfig class OrganizationsConfig(AppConfig): name = 'organizations' verbose_name = 'Organizations' django-organizations-1.0.0/organizations/forms.py0000644000076500000240000001623013214100213023471 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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 ugettext_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(OrganizationForm, self).__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(OrganizationForm, self).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(OrganizationUserAddForm, self).__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().MultipleObjectsReturned: raise forms.ValidationError(_("This email address has been used multiple times.")) 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=email): raise forms.ValidationError(_("There is already an organization " "member with this email address!")) 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(OrganizationAddForm, self).__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: 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() django-organizations-1.0.0/organizations/admin.py0000644000076500000240000000431513214100213023434 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.contrib import admin from organizations.base_admin import BaseOrganizationAdmin from organizations.base_admin import BaseOrganizationOwnerAdmin from organizations.base_admin import BaseOrganizationUserAdmin from organizations.base_admin import BaseOwnerInline from organizations.models import Organization from organizations.models import OrganizationOwner from organizations.models import OrganizationUser class OwnerInline(BaseOwnerInline): model = OrganizationOwner class OrganizationAdmin(BaseOrganizationAdmin): inlines = [OwnerInline] class OrganizationUserAdmin(BaseOrganizationUserAdmin): pass class OrganizationOwnerAdmin(BaseOrganizationOwnerAdmin): pass admin.site.register(Organization, OrganizationAdmin) admin.site.register(OrganizationUser, OrganizationUserAdmin) admin.site.register(OrganizationOwner, OrganizationOwnerAdmin) django-organizations-1.0.0/organizations/utils.py0000644000076500000240000000764713214100213023517 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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 try: # Django 1.9 org_user_model = org_model.organization_users.rel.related_model except AttributeError: # Django 1.8 org_user_model = org_model.organization_users.related.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) django-organizations-1.0.0/organizations/templates/0000755000076500000240000000000013214100244023771 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templates/organizations/0000755000076500000240000000000013214100244026660 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templates/organizations/register_success.html0000644000076500000240000000001012527730133033125 0ustar benlopatinstaff00000000000000Thanks! django-organizations-1.0.0/organizations/templates/organizations/organization_list.html0000644000076500000240000000037512527730133033326 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

organizations

{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/register_form.html0000644000076500000240000000016012527730133032426 0ustar benlopatinstaff00000000000000
{% csrf_token %} {{ form }}
django-organizations-1.0.0/organizations/templates/organizations/organization_form.html0000644000076500000240000000032712527730133033313 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/organizationuser_remind.html0000644000076500000240000000033612527730133034525 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/login.html0000644000076500000240000000060612643266033030676 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

Log in to your dashboard

{% csrf_token %} {{ form }}

Forgotten your password? Reset it.

{% endblock %} ././@LongLink0000000000000000000000000000014600000000000011216 Lustar 00000000000000django-organizations-1.0.0/organizations/templates/organizations/organizationuser_confirm_delete.htmldjango-organizations-1.0.0/organizations/templates/organizations/organizationuser_confirm_delete.htm0000644000076500000240000000032712527730133036052 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

{% csrf_token %} {{ form }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/organizationuser_form.html0000644000076500000240000000056612527730133034217 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %} {% if profile %}

Update your profile

{% else %}

{{ organization_user }} @ {{ organization }}

{% endif %} {% if user == organization_user.user %}This is you!{% endif %}
{% csrf_token %} {{ form }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/organization_users.html0000644000076500000240000000040212527730133033503 0ustar benlopatinstaff00000000000000
    {% for organization_user in organization_users %}
  • {{ organization_user }} {% if not organization_user.user.is_active %}Send reminder{% endif %}
  • {% endfor %}
django-organizations-1.0.0/organizations/templates/organizations/organizationuser_list.html0000644000076500000240000000042012643266033034216 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load org_tags %} {% block content %}

{{ organization }}'s Members

{% organization_users organization %} {% endblock %} django-organizations-1.0.0/organizations/templates/organizations/organization_detail.html0000644000076500000240000000064712643266033033621 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% load org_tags %} {% block content %}

{{ organization }}

{% organization_users organization %} {% endblock %} django-organizations-1.0.0/organizations/templates/organizations/organizationuser_detail.html0000644000076500000240000000104112643266033034505 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization_user }} @ {{ organization }}

{% if user == organization_user.user %}This is you!{% endif %}
  • Name: {{ organization_user.full_name }}
  • Email: {{ organization_user.user.email }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations/email/0000755000076500000240000000000013214100244027747 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templates/organizations/email/notification_body.html0000644000076500000240000000045713051627536034367 0ustar benlopatinstaff00000000000000You'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. django-organizations-1.0.0/organizations/templates/organizations/email/invitation_subject.txt0000644000076500000240000000006612527730133034431 0ustar benlopatinstaff00000000000000{% spaceless %}You've been invited!{% endspaceless %} django-organizations-1.0.0/organizations/templates/organizations/email/reminder_subject.txt0000644000076500000240000000002012527730133034040 0ustar benlopatinstaff00000000000000Just a reminder django-organizations-1.0.0/organizations/templates/organizations/email/activation_body.html0000644000076500000240000000000012527730133034015 0ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templates/organizations/email/activation_subject.txt0000644000076500000240000000000012527730133034372 0ustar benlopatinstaff00000000000000django-organizations-1.0.0/organizations/templates/organizations/email/reminder_body.html0000644000076500000240000000047512643266033033503 0ustar benlopatinstaff00000000000000You'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.id token %} If you are unsure about this link please contact the sender. django-organizations-1.0.0/organizations/templates/organizations/email/notification_subject.txt0000644000076500000240000000010613051627536034733 0ustar benlopatinstaff00000000000000{% spaceless %}You've been added to an organization{% endspaceless %} django-organizations-1.0.0/organizations/templates/organizations/email/invitation_body.html0000644000076500000240000000047512643266033034062 0ustar benlopatinstaff00000000000000You'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.id token %} If you are unsure about this link please contact the sender. django-organizations-1.0.0/organizations/templates/organizations/organization_confirm_delete.html0000644000076500000240000000042312527730133035324 0ustar benlopatinstaff00000000000000{% extends "organizations_base.html" %} {% block content %}

{{ organization }}

Are you sure you want to delete this organization?

{% csrf_token %} {{ form }}
{% endblock %} django-organizations-1.0.0/organizations/templates/organizations_base.html0000644000076500000240000000004212527730133030550 0ustar benlopatinstaff00000000000000{% block content %}{% endblock %} django-organizations-1.0.0/organizations/exceptions.py0000644000076500000240000000325212527730133024544 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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 django-organizations-1.0.0/organizations/urls.py0000644000076500000240000000633513214100213023335 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.conf.urls import url from django.contrib.auth.decorators import login_required from organizations import views urlpatterns = [ # Organization URLs url(r'^$', view=login_required(views.OrganizationList.as_view()), name="organization_list"), url(r'^add/$', view=login_required(views.OrganizationCreate.as_view()), name="organization_add"), url(r'^(?P[\d]+)/$', view=login_required(views.OrganizationDetail.as_view()), name="organization_detail"), url(r'^(?P[\d]+)/edit/$', view=login_required(views.OrganizationUpdate.as_view()), name="organization_edit"), url(r'^(?P[\d]+)/delete/$', view=login_required(views.OrganizationDelete.as_view()), name="organization_delete"), # Organization user URLs url(r'^(?P[\d]+)/people/$', view=login_required(views.OrganizationUserList.as_view()), name="organization_user_list"), url(r'^(?P[\d]+)/people/add/$', view=login_required(views.OrganizationUserCreate.as_view()), name="organization_user_add"), url(r'^(?P[\d]+)/people/(?P[\d]+)/remind/$', view=login_required(views.OrganizationUserRemind.as_view()), name="organization_user_remind"), url(r'^(?P[\d]+)/people/(?P[\d]+)/$', view=login_required(views.OrganizationUserDetail.as_view()), name="organization_user_detail"), url(r'^(?P[\d]+)/people/(?P[\d]+)/edit/$', view=login_required(views.OrganizationUserUpdate.as_view()), name="organization_user_edit"), url(r'^(?P[\d]+)/people/(?P[\d]+)/delete/$', view=login_required(views.OrganizationUserDelete.as_view()), name="organization_user_delete"), ] django-organizations-1.0.0/organizations/base.py0000644000076500000240000002224413214100213023257 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. 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.utils.translation import ugettext_lazy as _ from organizations.compat import six from organizations.managers import ActiveOrgManager from organizations.managers import OrgManager USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User') class UnicodeMixin(object): """ Python 2 and 3 string representation support. """ def __str__(self): if six.PY3: return self.__unicode__() else: return unicode(self).encode('utf-8') # noqa class OrgMeta(ModelBase): """ Base metaclass for dynamically linking related organization models. This is particularly useful for custom organizations that can avoid multitable inheritence 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'] 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, } 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' 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) 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")) 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)) class AbstractBaseOrganization(UnicodeMixin, 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 __unicode__(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 class AbstractBaseOrganizationUser(UnicodeMixin, 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 __unicode__(self): return u"{0} ({1})".format(self.user.get_full_name() if self.user.is_active else self.user.email, 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). """ if hasattr(self.user, 'get_full_name'): return self.user.get_full_name() return "{0}".format(self.user) class OrganizationUserBase(six.with_metaclass(OrgMeta, AbstractBaseOrganizationUser)): class Meta(AbstractBaseOrganizationUser.Meta): abstract = True class AbstractBaseOrganizationOwner(UnicodeMixin, models.Model): """ Each organization must have one and only one organization owner. """ class Meta: abstract = True def __unicode__(self): return u"{0}: {1}".format(self.organization, self.organization_user) class OrganizationOwnerBase(six.with_metaclass(OrgMeta, AbstractBaseOrganizationOwner)): class Meta(AbstractBaseOrganizationOwner.Meta): abstract = True django-organizations-1.0.0/organizations/views.py0000644000076500000240000002154213214100213023502 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.contrib.sites.shortcuts import get_current_site from django.http import HttpResponseBadRequest from django.shortcuts import redirect from django.shortcuts import render from django.utils.translation import ugettext 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.compat import reverse 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.mixins import AdminRequiredMixin from organizations.mixins import MembershipRequiredMixin from organizations.mixins import OrganizationMixin from organizations.mixins import OrganizationUserMixin from organizations.mixins import OwnerRequiredMixin from organizations.models import Organization from organizations.utils import create_organization class BaseOrganizationList(ListView): # TODO change this to query on the specified model queryset = Organization.active.all() context_object_name = "organizations" def get_queryset(self): return super(BaseOrganizationList, self).get_queryset().filter(users=self.request.user) class BaseOrganizationDetail(OrganizationMixin, DetailView): def get_context_data(self, **kwargs): context = super(BaseOrganizationDetail, self).get_context_data(**kwargs) context['organization_users'] = self.organization.organization_users.all() context['organization'] = self.organization return context class BaseOrganizationCreate(CreateView): model = Organization 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(BaseOrganizationCreate, self).get_form_kwargs() kwargs.update({'request': self.request}) return kwargs class BaseOrganizationUpdate(OrganizationMixin, UpdateView): form_class = OrganizationForm def get_form_kwargs(self): kwargs = super(BaseOrganizationUpdate, self).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(BaseOrganizationUserCreate, self).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(BaseOrganizationUserCreate, self).get(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.organization = self.get_object() return super(BaseOrganizationUserCreate, self).post(request, *args, **kwargs) class BaseOrganizationUserRemind(OrganizationUserMixin, DetailView): template_name = 'organizations/organizationuser_remind.html' # TODO move to invitations backend? def get_object(self, **kwargs): self.organization_user = super(BaseOrganizationUserRemind, self).get_object() if self.organization_user.user.is_active: raise HttpResponseBadRequest(_("User is already active")) return self.organization_user def post(self, request, *args, **kwargs): self.object = self.get_object() invitation_backend().send_reminder(self.object.user, **{'domain': get_current_site(self.request), 'organization': self.organization, 'sender': request.user}) return redirect(self.object) 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): try: if request.user.is_authenticated(): return redirect('organization_add') except TypeError: if request.user.is_authenticated: return redirect('organization_add') return super(OrganizationSignup, self).dispatch(request, *args, **kwargs) def get_success_url(self): if hasattr(self, 'success_url'): return self.success_url return reverse('organization_signup_success') def form_valid(self, form): """ """ 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()) def signup_success(self, request): return render(request, "organizations/signup_success.html", {}) class OrganizationList(BaseOrganizationList): pass class OrganizationCreate(BaseOrganizationCreate): """ Allows any user to create a new organization. """ pass class OrganizationDetail(MembershipRequiredMixin, BaseOrganizationDetail): pass class OrganizationUpdate(AdminRequiredMixin, BaseOrganizationUpdate): pass class OrganizationDelete(OwnerRequiredMixin, BaseOrganizationDelete): pass class OrganizationUserList(MembershipRequiredMixin, BaseOrganizationUserList): pass class OrganizationUserDetail(AdminRequiredMixin, BaseOrganizationUserDetail): pass class OrganizationUserUpdate(AdminRequiredMixin, BaseOrganizationUserUpdate): pass class OrganizationUserCreate(AdminRequiredMixin, BaseOrganizationUserCreate): pass class OrganizationUserRemind(AdminRequiredMixin, BaseOrganizationUserRemind): pass class OrganizationUserDelete(AdminRequiredMixin, BaseOrganizationUserDelete): pass django-organizations-1.0.0/organizations/managers.py0000644000076500000240000000432212527730133024157 0ustar benlopatinstaff00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2012-2015, 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. from django.db import models class OrgManager(models.Manager): def get_for_user(self, user): if hasattr(self, 'get_queryset'): return self.get_queryset().filter(users=user) else: # Deprecated method for older versions of Django return self.get_query_set().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): try: return super(ActiveOrgManager, self).get_queryset().filter(is_active=True) except AttributeError: # Deprecated method for older versions of Django. return super(ActiveOrgManager, self).get_query_set().filter(is_active=True) get_query_set = get_queryset django-organizations-1.0.0/LICENSE0000644000076500000240000000243212643266231020130 0ustar benlopatinstaff00000000000000Copyright (c) 2012-2016, 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. django-organizations-1.0.0/MANIFEST.in0000644000076500000240000000021012345650006020645 0ustar benlopatinstaff00000000000000include setup.py include README.rst include MANIFEST.in include HISTORY.rst include LICENSE recursive-include organizations/templates * django-organizations-1.0.0/setup.py0000644000076500000240000000330613214100213020614 0ustar benlopatinstaff00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import os import sys import organizations try: from setuptools import setup except ImportError: from distutils.core import setup if sys.argv[-1] == 'publish': os.system('python setup.py sdist upload') os.system('python setup.py bdist_wheel upload') sys.exit() readme = open('README.rst').read() history = open('HISTORY.rst').read().replace('.. :changelog:', '') setup( author="Ben Lopatin", author_email="ben@wellfire.co", name='django-organizations', version=organizations.__version__, description='Group accounts for Django', long_description=readme + '\n\n' + history, url='https://github.com/bennylope/django-organizations/', license='BSD License', platforms=['OS Independent'], packages=[ 'organizations', 'organizations.backends', 'organizations.migrations', 'organizations.templatetags', ], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Framework :: Django', ], install_requires=[ 'Django>=1.8.0', ], test_suite='tests', include_package_data=True, zip_safe=False, ) django-organizations-1.0.0/HISTORY.rst0000644000076500000240000001046113214100213020775 0ustar benlopatinstaff00000000000000.. :changelog: History ======= 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) 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 django-organizations-1.0.0/django_organizations.egg-info/0000755000076500000240000000000013214100244025007 5ustar benlopatinstaff00000000000000django-organizations-1.0.0/django_organizations.egg-info/PKG-INFO0000644000076500000240000004455013214100244026114 0ustar benlopatinstaff00000000000000Metadata-Version: 1.1 Name: django-organizations Version: 1.0.0 Summary: Group accounts for Django Home-page: https://github.com/bennylope/django-organizations/ Author: Ben Lopatin Author-email: ben@wellfire.co 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 / https://wellfire.co) * - 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 `_ Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations 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:: 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:: 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:: ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative (note: this is not compatible with Django 2.0):: 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 `_ Note that as of django-autoslug 1.9.3. it is incompatible with Django 2.x 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:: 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:: >>> 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. Development & Contributing ========================== 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 * Python 3 readiness 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 1.11.x against Python 2.7, 3.4, 3.5, 3.6, and PyPy * Django 2.0.x against Python 3.4, 3.5, 3.6 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. License ======= Anyone is free to use or modify this software under the terms of the BSD license. History ======= 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) 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 Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Framework :: Django django-organizations-1.0.0/django_organizations.egg-info/not-zip-safe0000644000076500000240000000000113214100244027235 0ustar benlopatinstaff00000000000000 django-organizations-1.0.0/django_organizations.egg-info/SOURCES.txt0000644000076500000240000000470613214100244026702 0ustar benlopatinstaff00000000000000HISTORY.rst LICENSE MANIFEST.in README.rst setup.cfg setup.py django_organizations.egg-info/PKG-INFO django_organizations.egg-info/SOURCES.txt django_organizations.egg-info/dependency_links.txt django_organizations.egg-info/not-zip-safe django_organizations.egg-info/requires.txt django_organizations.egg-info/top_level.txt organizations/__init__.py organizations/abstract.py organizations/admin.py organizations/app_settings.py organizations/apps.py organizations/base.py organizations/base_admin.py organizations/compat.py organizations/exceptions.py organizations/fields.py organizations/forms.py organizations/managers.py organizations/mixins.py organizations/models.py organizations/signals.py organizations/urls.py organizations/utils.py organizations/views.py organizations/backends/__init__.py organizations/backends/defaults.py organizations/backends/forms.py organizations/backends/tokens.py organizations/migrations/0001_initial.py organizations/migrations/0002_model_update.py organizations/migrations/__init__.py organizations/templates/organizations_base.html organizations/templates/organizations/login.html organizations/templates/organizations/organization_confirm_delete.html organizations/templates/organizations/organization_detail.html organizations/templates/organizations/organization_form.html organizations/templates/organizations/organization_list.html organizations/templates/organizations/organization_users.html organizations/templates/organizations/organizationuser_confirm_delete.html organizations/templates/organizations/organizationuser_detail.html organizations/templates/organizations/organizationuser_form.html organizations/templates/organizations/organizationuser_list.html organizations/templates/organizations/organizationuser_remind.html organizations/templates/organizations/register_form.html organizations/templates/organizations/register_success.html organizations/templates/organizations/email/activation_body.html organizations/templates/organizations/email/activation_subject.txt organizations/templates/organizations/email/invitation_body.html organizations/templates/organizations/email/invitation_subject.txt organizations/templates/organizations/email/notification_body.html organizations/templates/organizations/email/notification_subject.txt organizations/templates/organizations/email/reminder_body.html organizations/templates/organizations/email/reminder_subject.txt organizations/templatetags/__init__.py organizations/templatetags/org_tags.pydjango-organizations-1.0.0/django_organizations.egg-info/requires.txt0000644000076500000240000000001613214100244027404 0ustar benlopatinstaff00000000000000Django>=1.8.0 django-organizations-1.0.0/django_organizations.egg-info/top_level.txt0000644000076500000240000000001613214100244027536 0ustar benlopatinstaff00000000000000organizations django-organizations-1.0.0/django_organizations.egg-info/dependency_links.txt0000644000076500000240000000000113214100244031055 0ustar benlopatinstaff00000000000000 django-organizations-1.0.0/setup.cfg0000644000076500000240000000010313214100244020717 0ustar benlopatinstaff00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 django-organizations-1.0.0/README.rst0000644000076500000240000002242013214100213020567 0ustar benlopatinstaff00000000000000==================== django-organizations ==================== .. start-table .. list-table:: :stub-columns: 1 * - Summary - Groups and multi-user account management * - Author - Ben Lopatin (http://benlopatin.com / https://wellfire.co) * - 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 `_ Installing ========== First add the application to your Python path. The easiest way is to use `pip`:: pip install django-organizations 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:: 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:: 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:: ORGS_SLUGFIELD = 'django_extensions.db.fields.AutoSlugField' Alternative (note: this is not compatible with Django 2.0):: 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 `_ Note that as of django-autoslug 1.9.3. it is incompatible with Django 2.x 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:: 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:: >>> 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. Development & Contributing ========================== 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 * Python 3 readiness 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 1.11.x against Python 2.7, 3.4, 3.5, 3.6, and PyPy * Django 2.0.x against Python 3.4, 3.5, 3.6 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. License ======= Anyone is free to use or modify this software under the terms of the BSD license.