├── demo ├── demo │ ├── __init__.py │ ├── templates │ │ ├── userena │ │ │ ├── base_userena.html │ │ │ ├── emails │ │ │ │ └── activation_email_message.html │ │ │ ├── profile_list.html │ │ │ ├── email_form.html │ │ │ ├── password_form.html │ │ │ ├── profile_form.html │ │ │ └── profile_detail.html │ │ ├── 404.html │ │ ├── umessages │ │ │ ├── message_detail.html │ │ │ └── message_list.html │ │ ├── 500.html │ │ ├── static │ │ │ └── promo.html │ │ └── base.html │ ├── static │ │ ├── img │ │ │ ├── crew.png │ │ │ ├── grid.png │ │ │ ├── try.png │ │ │ ├── forkme.png │ │ │ ├── lissy.png │ │ │ ├── stripe.png │ │ │ ├── trans.png │ │ │ ├── icon-demo.png │ │ │ ├── icon-docs.png │ │ │ ├── userena.png │ │ │ ├── userina.png │ │ │ ├── arrow-down.png │ │ │ ├── icon-browse.png │ │ │ ├── bg-container.png │ │ │ ├── icon-download.png │ │ │ └── bread-and-pepper-logo.png │ │ └── css │ │ │ └── reset.css │ ├── urls.py │ ├── wsgi.py │ ├── settings_dotcloud.py │ └── settings.py ├── profiles │ ├── __init__.py │ ├── views.py │ ├── forms.py │ └── models.py ├── private │ └── .gitignore ├── public │ ├── media │ │ └── .gitignore │ └── static │ │ └── .gitignore ├── dotcloud.yml ├── README.md ├── nginx.conf ├── locale │ └── nl │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── wsgi.py ├── superuser.py ├── requirements.txt ├── postinstall ├── manage.py └── createdb.py ├── .venv ├── userena ├── contrib │ ├── __init__.py │ └── umessages │ │ ├── __init__.py │ │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ │ ├── templatetags │ │ ├── __init__.py │ │ └── umessages_tags.py │ │ ├── south_migrations │ │ └── __init__.py │ │ ├── templates │ │ ├── umessages │ │ │ ├── base_message.html │ │ │ ├── message_detail.html │ │ │ ├── message_form.html │ │ │ └── message_list.html │ │ └── base.html │ │ ├── signals.py │ │ ├── tests │ │ ├── __init__.py │ │ ├── test_forms.py │ │ ├── test_managers.py │ │ ├── test_models.py │ │ └── test_fields.py │ │ ├── admin.py │ │ ├── urls.py │ │ ├── fixtures │ │ └── messages.json │ │ ├── forms.py │ │ ├── fields.py │ │ └── managers.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── clean_expired.py │ │ └── check_permissions.py ├── migrations │ ├── __init__.py │ ├── 0002_auto_20170214_1529.py │ ├── 0003_auto_20170217_1640.py │ └── 0001_initial.py ├── south_migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests │ ├── profiles │ │ ├── __init__.py │ │ └── models.py │ ├── __init__.py │ ├── tests_urls.py │ ├── tests_decorators.py │ ├── test_backends.py │ ├── tests_middleware.py │ ├── test_privacy.py │ ├── tests_utils.py │ └── test_commands.py ├── runtests │ ├── __init__.py │ ├── private │ │ └── development.db │ ├── templates │ │ └── 404.html │ ├── urls.py │ └── runtests.py ├── templates │ ├── userena │ │ ├── base_userena.html │ │ ├── emails │ │ │ ├── activation_email_subject.txt │ │ │ ├── invitation_email_subject.txt │ │ │ ├── confirmation_email_subject_new.txt │ │ │ ├── confirmation_email_subject_old.txt │ │ │ ├── confirmation_email_subject_new.html │ │ │ ├── invitation_email_message.txt │ │ │ ├── invitation_email_message.html │ │ │ ├── activation_email_message.txt │ │ │ ├── confirmation_email_message_old.txt │ │ │ ├── confirmation_email_message_new.txt │ │ │ ├── password_reset_message.txt │ │ │ ├── activation_email_message.html │ │ │ ├── confirmation_email_message_old.html │ │ │ ├── confirmation_email_message_new.html │ │ │ └── password_reset_message.html │ │ ├── signout.html │ │ ├── password_complete.html │ │ ├── password_reset_done.html │ │ ├── email_confirm_complete.html │ │ ├── password_reset_complete.html │ │ ├── email_confirm_fail.html │ │ ├── password_reset_form.html │ │ ├── password_reset_confirm_form.html │ │ ├── disabled.html │ │ ├── activate_fail.html │ │ ├── activate_retry.html │ │ ├── email_change_complete.html │ │ ├── activate_retry_success.html │ │ ├── signup_complete.html │ │ ├── signup_form.html │ │ ├── invite_new_user.html │ │ ├── email_form.html │ │ ├── signin_form.html │ │ ├── password_form.html │ │ ├── profile_form.html │ │ ├── profile_list.html │ │ ├── list_invited_users.html │ │ └── profile_detail.html │ └── base.html ├── locale │ ├── bg │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── de │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── el │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── es │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── it │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── nb │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── nl │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── pl │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── pt │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── ro │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── ru │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── tr │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── de_du │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ └── django.mo │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ └── django.mo │ └── zh_TW │ │ └── LC_MESSAGES │ │ └── django.mo ├── apps.py ├── __init__.py ├── signals.py ├── fixtures │ ├── profiles.json │ └── users.json ├── admin.py ├── decorators.py ├── middleware.py ├── backends.py ├── compat.py ├── mail.py ├── settings.py └── urls.py ├── AUTHORS ├── run_tests ├── docs ├── contrib │ └── umessages │ │ ├── api │ │ ├── index.rst │ │ ├── managers.rst │ │ └── views.rst │ │ └── index.rst ├── api │ ├── backends.rst │ ├── index.rst │ ├── decorators.rst │ ├── middleware.rst │ ├── managers.rst │ ├── utils.rst │ ├── models.rst │ ├── forms.rst │ └── views.rst ├── commands.rst ├── signals.rst ├── index.rst └── Makefile ├── setup.cfg ├── .gitignore ├── MANIFEST.in ├── .coveragerc ├── README.md ├── .travis.yml ├── tox.ini ├── LICENSE ├── CREDITS ├── setup.py └── UPDATES.md /demo/demo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.venv: -------------------------------------------------------------------------------- 1 | django-userena 2 | -------------------------------------------------------------------------------- /demo/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/contrib/umessages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/tests/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Petar Radosevic: http://wunki.org 2 | -------------------------------------------------------------------------------- /demo/private/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | tox 4 | -------------------------------------------------------------------------------- /userena/contrib/umessages/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/public/media/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /demo/public/static/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/contrib/umessages/south_migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /userena/runtests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /userena/templates/userena/base_userena.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /demo/dotcloud.yml: -------------------------------------------------------------------------------- 1 | www: 2 | type: python 3 | db: 4 | type: postgresql 5 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/base_userena.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templates/umessages/base_message.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | -------------------------------------------------------------------------------- /demo/demo/static/img/crew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/crew.png -------------------------------------------------------------------------------- /demo/demo/static/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/grid.png -------------------------------------------------------------------------------- /demo/demo/static/img/try.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/try.png -------------------------------------------------------------------------------- /demo/demo/static/img/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/forkme.png -------------------------------------------------------------------------------- /demo/demo/static/img/lissy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/lissy.png -------------------------------------------------------------------------------- /demo/demo/static/img/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/stripe.png -------------------------------------------------------------------------------- /demo/demo/static/img/trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/trans.png -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Userena Demo 2 | 3 | You can use this project to see how to setup Django userena for your own 4 | project. 5 | -------------------------------------------------------------------------------- /demo/demo/static/img/icon-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/icon-demo.png -------------------------------------------------------------------------------- /demo/demo/static/img/icon-docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/icon-docs.png -------------------------------------------------------------------------------- /demo/demo/static/img/userena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/userena.png -------------------------------------------------------------------------------- /demo/demo/static/img/userina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/userina.png -------------------------------------------------------------------------------- /demo/nginx.conf: -------------------------------------------------------------------------------- 1 | location /media/ { root /home/dotcloud/data ; } 2 | location /static/ { root /home/dotcloud/volatile ; } 3 | -------------------------------------------------------------------------------- /userena/contrib/umessages/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | email_sent = Signal(providing_args=["msg"]) 4 | -------------------------------------------------------------------------------- /demo/demo/static/img/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/arrow-down.png -------------------------------------------------------------------------------- /demo/demo/static/img/icon-browse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/icon-browse.png -------------------------------------------------------------------------------- /demo/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /demo/demo/static/img/bg-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/bg-container.png -------------------------------------------------------------------------------- /demo/demo/static/img/icon-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/icon-download.png -------------------------------------------------------------------------------- /userena/locale/bg/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/bg/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/el/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/el/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/es/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/es/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/it/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/it/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/nl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/nl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/pt/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/pt/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/ro/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/ro/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/tr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/tr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/runtests/private/development.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/runtests/private/development.db -------------------------------------------------------------------------------- /demo/profiles/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def promo(request): 5 | return render(request, 'static/promo.html') 6 | -------------------------------------------------------------------------------- /userena/locale/de_du/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/de_du/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/zh_CN/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/zh_CN/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /userena/locale/zh_TW/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/userena/locale/zh_TW/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /demo/demo/static/img/bread-and-pepper-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chozabu/django-userena/master/demo/demo/static/img/bread-and-pepper-logo.png -------------------------------------------------------------------------------- /userena/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserenaConfig(AppConfig): 5 | name = 'userena' 6 | verbose_name = 'Userena' 7 | -------------------------------------------------------------------------------- /userena/runtests/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site.name as site %}Your signup at {{ site }}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/invitation_email_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site.name as site %}Your invited to {{ site }}.{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /docs/contrib/umessages/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _umessages-api: 2 | 3 | API Reference 4 | ============= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | managers 10 | views 11 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_subject_new.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site.name as site %}Email verification for {{ site }}.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = docs/ 3 | build-dir = docs/build 4 | all_files = 1 5 | 6 | [upload_sphinx] 7 | upload-dir = docs/build/html 8 | 9 | [bdist_wheel] 10 | universal = 1 11 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_subject_old.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site.name as site %}A request to change your email address is made at {{ site }}.{% endblocktrans %} 3 | 4 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_subject_new.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | 4 | {% blocktrans with site.name as site %}Email verification for {{ site }}.{% endblocktrans %} 5 | 6 | -------------------------------------------------------------------------------- /docs/api/backends.rst: -------------------------------------------------------------------------------- 1 | .. _api-backends: 2 | 3 | Backends 4 | ======== 5 | 6 | .. automodule:: userena.backends 7 | 8 | Return to :ref:`api`. 9 | 10 | .. autoclass:: userena.backends.UserenaAuthenticationBackend 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API Reference 4 | ============= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | backends 10 | decorators 11 | forms 12 | managers 13 | middleware 14 | models 15 | utils 16 | views 17 | -------------------------------------------------------------------------------- /userena/contrib/umessages/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | if django.VERSION < (1, 6): 4 | from .test_fields import * 5 | from .test_forms import * 6 | from .test_managers import * 7 | from .test_models import * 8 | from .test_views import * 9 | -------------------------------------------------------------------------------- /docs/api/decorators.rst: -------------------------------------------------------------------------------- 1 | .. _api-decorators: 2 | 3 | Decorators 4 | ========== 5 | 6 | .. automodule:: userena.decorators 7 | 8 | Return to :ref:`api`. 9 | 10 | secure_required 11 | --------------- 12 | 13 | .. autofunction:: userena.decorators.secure_required 14 | -------------------------------------------------------------------------------- /demo/wsgi.py: -------------------------------------------------------------------------------- 1 | import django.core.handlers.wsgi 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),'demo'))) 7 | os.environ['DJANGO_SETTINGS_MODULE'] = 'demo.settings_dotcloud' 8 | application = django.core.handlers.wsgi.WSGIHandler() 9 | -------------------------------------------------------------------------------- /docs/contrib/umessages/api/managers.rst: -------------------------------------------------------------------------------- 1 | .. _messages-api-managers: 2 | 3 | Managers 4 | ======== 5 | 6 | .. automodule:: userena.contrib.umessages.managers 7 | 8 | MessageManager 9 | -------------- 10 | 11 | .. autoclass:: userena.contrib.umessages.managers.MessageManager 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/middleware.rst: -------------------------------------------------------------------------------- 1 | .. _api-middleware: 2 | 3 | Middleware 4 | ========== 5 | 6 | .. automodule:: userena.middleware 7 | 8 | Return to :ref:`api` 9 | 10 | UserenaLocaleMiddleware 11 | ----------------------- 12 | 13 | .. autoclass:: userena.middleware.UserenaLocaleMiddleware 14 | :members: 15 | -------------------------------------------------------------------------------- /demo/superuser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from wsgi import * 3 | from django.contrib.auth import get_user_model 4 | try: 5 | wunki = get_user_model().objects.get(username='wunki') 6 | except get_user_model().DoesNotExist: 7 | pass 8 | else: 9 | wunki.is_staff = True 10 | wunki.is_superuser = True 11 | wunki.save() 12 | -------------------------------------------------------------------------------- /demo/demo/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% trans "Page not found" %}

{% endblock %} 5 | 6 | {% block content %} 7 |

{% trans "Page could not be found."%}

8 |

{% trans "If you think this is a bug in userena, please create an issue at Github." %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /userena/templates/userena/signout.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Signed out" %}{% endblock %} 5 | {% block content_title %}

{% trans "You have been signed out" %}.

{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "You have been signed out. Till we meet again." %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | # Django 2 | Django==1.4 3 | 4 | # Imaging library needed for easy_thumbnails 5 | http://effbot.org/downloads/Imaging-1.1.7.tar.gz 6 | 7 | # Thumbails used for mugshots 8 | easy-thumbnails 9 | 10 | # Django-guardian used for privacy settings 11 | django-guardian 12 | 13 | # Django-south for migrations 14 | South 15 | 16 | # Coverage for testing 17 | django_coverage 18 | -------------------------------------------------------------------------------- /userena/templates/userena/password_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password changed" %}{% endblock %} 5 | {% block content_title %}

{% trans "Your password has been changed" %}.

{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "From now on you can use your new password to signin" %}.

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /demo/postinstall: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script is to setup userena on dotcloud 4 | python createdb.py 5 | python superuser.py 6 | python manage.py syncdb --settings=demo.settings_dotcloud --noinput 7 | python manage.py migrate --settings=demo.settings_dotcloud 8 | mkdir -p /home/dotcloud/data/media /home/dotcloud/volatile/static 9 | python manage.py collectstatic --settings=demo.settings_dotcloud --noinput 10 | -------------------------------------------------------------------------------- /docs/api/managers.rst: -------------------------------------------------------------------------------- 1 | .. _api-managers: 2 | 3 | Managers 4 | ======== 5 | 6 | .. automodule:: userena.managers 7 | 8 | Return to :ref:`api` 9 | 10 | UserenaManager 11 | -------------- 12 | 13 | .. autoclass:: userena.managers.UserenaManager 14 | :members: 15 | 16 | UserenaBaseProfileManager 17 | ------------------------- 18 | 19 | .. autoclass:: userena.managers.UserenaBaseProfileManager 20 | :members: 21 | -------------------------------------------------------------------------------- /userena/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django accounts management made easy. 3 | 4 | """ 5 | default_app_config = 'userena.apps.UserenaConfig' 6 | 7 | VERSION = (2, 0, 1) 8 | 9 | __version__ = '.'.join((str(each) for each in VERSION[:4])) 10 | 11 | 12 | def get_version(): 13 | """ 14 | Returns string with digit parts only as version. 15 | 16 | """ 17 | return '.'.join((str(each) for each in VERSION[:3])) 18 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | cwd = os.path.dirname(__file__) 7 | sys.path.append(os.path.join(os.path.abspath(os.path.dirname(cwd)), '../')) 8 | 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 10 | 11 | from django.core.management import execute_from_command_line 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /userena/templates/userena/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password reset email sent" %}{% endblock %} 5 | {% block content_title %}

{% trans "Password reset email sent" %}

{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "An e-mail has been sent to you which explains how to reset your password." %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /userena/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | 3 | if django.VERSION < (1, 6): 4 | from .test_backends import * 5 | from .test_commands import * 6 | from .test_privacy import * 7 | from .tests_decorators import * 8 | from .tests_forms import * 9 | from .tests_managers import * 10 | from .tests_middleware import * 11 | from .tests_models import * 12 | from .tests_utils import * 13 | from .tests_views import * -------------------------------------------------------------------------------- /userena/templates/userena/email_confirm_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Email changed!" %}{% endblock %} 5 | 6 | {% block content_title %}

{% trans 'New email' %}

{% endblock %} 7 | {% block content %} 8 |

{% blocktrans with site.name as site %}Your new email address is saved. You can continue using {{ site }} with this email.{% endblocktrans %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/invitation_email_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {{ inviter.username }} has invited you to {{site.name}} 3 | {% trans "To activate your account you should click on the link below:" %} 4 | 5 | {{ protocol }}://{{ site.domain }}{% url 'userena_activate_invited_user' invitation_key %} 6 | 7 | {% trans "Serving you is our pleasure!" %} 8 | 9 | {% trans "Sincerely" %}, 10 | {{ site.name }} 11 | {% endautoescape %} 12 | -------------------------------------------------------------------------------- /userena/templates/userena/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Password reset" %}{% endblock %} 5 | {% block content_title %}

{% trans "Your password has been reset" %}

{% endblock %} 6 | {% block content %} 7 | {% url 'userena_signin' as signin_url %} 8 | {% blocktrans %}You can now signin with your new password.{% endblocktrans %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /userena/management/commands/clean_expired.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from userena.models import UserenaSignup 4 | 5 | class Command(BaseCommand): 6 | """ 7 | Search for users that still haven't verified their email after 8 | ``USERENA_ACTIVATION_DAYS`` and delete them. 9 | 10 | """ 11 | help = 'Deletes expired users.' 12 | def handle(self, *args, **kwargs): 13 | users = UserenaSignup.objects.delete_expired_users() 14 | -------------------------------------------------------------------------------- /userena/templates/userena/email_confirm_fail.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Email verification failed." %}{% endblock %} 5 | {% block content_title %}

{% trans "Your email could not be verified..." %}

{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Your email could not be verified..." %}

9 |

{% trans "You could try changing it again in your account settings." %}

10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /userena/templates/userena/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Reset password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 | {% trans "Reset Password" %} 10 | {% csrf_token %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /userena/templates/userena/password_reset_confirm_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Reset password" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 | {% trans "Reset Password" %} 10 | {% csrf_token %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /userena/templates/userena/disabled.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Disabled account" %}{% endblock %} 5 | {% block content_title %}

{% trans "Your account has been disabled" %}

{% endblock %} 6 | {% block content %} 7 |

{% trans "It seems your account has been disabled." %}

8 |

{% trans "If you feel that injustice has been done to you, feel free to contact the administrators to find out why" %}

9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /docs/api/utils.rst: -------------------------------------------------------------------------------- 1 | .. _api-utils: 2 | 3 | Utils 4 | ===== 5 | 6 | .. automodule:: userena.utils 7 | 8 | get_gravatar 9 | ------------ 10 | 11 | .. autofunction:: userena.utils.get_gravatar 12 | 13 | signin_redirect 14 | --------------- 15 | 16 | .. autofunction:: userena.utils.signin_redirect 17 | 18 | generate_sha1 19 | ------------- 20 | 21 | .. autofunction:: userena.utils.generate_sha1 22 | 23 | get_profile_model 24 | ----------------- 25 | 26 | .. autofunction:: userena.utils.get_profile_model 27 | 28 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templates/umessages/message_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'umessages/base_message.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% blocktrans %}Conversation with {{ recipient }}{% endblocktrans %}

{% endblock %} 5 | 6 | {% block content %} 7 | 8 | 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templates/umessages/message_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'umessages/base_message.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% trans "Compose" %}

{% endblock %} 5 | 6 | {% block content %} 7 |
8 | {% csrf_token %} 9 |
10 | {% trans "Compose message" %} 11 | {{ form.as_p }} 12 |
13 | 14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /userena/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | signup_complete = Signal(providing_args=["user",]) 4 | activation_complete = Signal(providing_args=["user",]) 5 | confirmation_complete = Signal(providing_args=["user","old_email"]) 6 | password_complete = Signal(providing_args=["user",]) 7 | email_change = Signal(providing_args=["user","prev_email","new_email"]) 8 | profile_change = Signal(providing_args=["user",]) 9 | account_signin = Signal(providing_args=["user",]) 10 | account_signout = Signal(providing_args=["user",]) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Setup.py 2 | *.egg 3 | *egg-info 4 | # Documentation 5 | _build 6 | build 7 | .Python 8 | 9 | # Virtual environment 10 | bin/ 11 | lib/ 12 | lib64 13 | include/ 14 | src/ 15 | dist/ 16 | 17 | # Certificates 18 | certs/ 19 | 20 | # Production server 21 | logs/ 22 | pip-log.txt 23 | 24 | # Testing 25 | .coverage 26 | .tox 27 | 28 | # Demo project 29 | demo/demo_project.db 30 | demo/settings_production.py 31 | demo/media/admin 32 | 33 | # Compiled 34 | *.pyc 35 | 36 | # Temp vim files 37 | *.swp 38 | 39 | # Backup files 40 | *~ 41 | -------------------------------------------------------------------------------- /userena/templates/userena/activate_fail.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account activation failed." %}{% endblock %} 5 | {% block content_title %}

{% trans "Your account could not be activated..." %}

{% endblock %} 6 | 7 | {% block content %} 8 |

{% trans "Your account could not be activated. This could be because your activation link has aged." %}

9 |

{% trans "Please try signing up again or contact us if you think something went wrong." %}

10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /userena/templates/userena/activate_retry.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | 5 | {% block title %}{% trans "Account activation failed." %}{% endblock %} 6 | {% block content_title %}

{% trans "Your account could not be activated..." %}

{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Your account could not be activated because activation link is expired." %}

10 |

{% trans "Request a new activation link." %}

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/invitation_email_message.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 |

{{ inviter.username }} has invited you to {{site.name}}

5 |

6 | {% trans "To activate your account you should click on the link below:" %}
7 | {{ protocol }}://{{ site.domain }}{% url 'userena_activate_invited_user' invitation_key %} 8 | 9 |

10 |

11 | {% trans "Serving you is our pleasure!" %}
12 | 13 | {% trans "Sincerely" %},
14 | {{ site.name }} 15 |

16 | 17 | 18 | {% endautoescape %} 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include MANIFEST.in 3 | include tests.py 4 | include run_tests 5 | include pip-requirements.txt 6 | recursive-include userena/fixtures *.json 7 | recursive-include userena/templates *.html *.txt 8 | recursive-include userena/locale *.po *.mo 9 | recursive-include userena/runtests/templates *.html *.txt 10 | recursive-include demo_project *.py 11 | recursive-include demo_project/templates *.html 12 | recursive-include userena/contrib/umessages/fixtures *.json 13 | recursive-include userena/contrib/umessages/templates *.html 14 | recursive-include docs * 15 | -------------------------------------------------------------------------------- /docs/contrib/umessages/index.rst: -------------------------------------------------------------------------------- 1 | .. _umessages: 2 | 3 | uMessages 4 | ========= 5 | 6 | Userena's umesagges supplies you with iPhone like messaging system for your 7 | users. 8 | 9 | 10 | Installation 11 | ------------ 12 | 13 | You install it by adding ``userena.contrib.umessages`` to your 14 | ``INSTALLED_APPS`` setting. You also need to add it to your urlconf. For 15 | example:: 16 | 17 | (r'^messages/', include('userena.contrib.umessages.urls')), 18 | 19 | A ``syncdb`` later and you have a great messaging system for in your 20 | application. 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | api/index 26 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/activation_email_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% if not without_usernames %}{% blocktrans with user.username as username %}Dear {{ username }},{% endblocktrans %} 3 | {% endif %} 4 | {% blocktrans with site.name as site %}Thank you for signing up at {{ site }}.{% endblocktrans %} 5 | 6 | {% trans "To activate your account you should click on the link below:" %} 7 | 8 | {{ protocol }}://{{ site.domain }}{% url 'userena_activate' activation_key %} 9 | 10 | {% trans "Thanks for using our site!" %} 11 | 12 | {% trans "Sincerely" %}, 13 | {{ site.name }} 14 | {% endautoescape %} 15 | -------------------------------------------------------------------------------- /userena/fixtures/profiles.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, 2 | "model": "profiles.profile", 3 | "fields": {"gender": 1, 4 | "website": "", 5 | "user": 1, 6 | "mugshot": "", 7 | "location": "Amsterdam", 8 | "privacy": "open", 9 | "language": "nl" 10 | } 11 | }, 12 | { 13 | "pk": 2, 14 | "model": "profiles.profile", 15 | "fields": {"gender": 2, 16 | "website": "", 17 | "user": 2, 18 | "mugshot": "", 19 | "location": "New York", 20 | "privacy": "open", 21 | "language": "en" 22 | } 23 | }] 24 | -------------------------------------------------------------------------------- /docs/contrib/umessages/api/views.rst: -------------------------------------------------------------------------------- 1 | .. _messages-api-views: 2 | 3 | Views 4 | ===== 5 | 6 | .. automodule:: userena.contrib.umessages.views 7 | 8 | MessageListView 9 | --------------- 10 | 11 | .. autofunction:: userena.contrib.umessages.views.MessageListView 12 | 13 | MessageDetailListView 14 | --------------------- 15 | 16 | .. autofunction:: userena.contrib.umessages.views.MessageDetailListView 17 | 18 | message_compose 19 | --------------- 20 | 21 | .. autofunction:: userena.contrib.umessages.views.message_compose 22 | 23 | message_remove 24 | -------------- 25 | 26 | .. autofunction:: userena.contrib.umessages.views.message_remove 27 | -------------------------------------------------------------------------------- /docs/api/models.rst: -------------------------------------------------------------------------------- 1 | .. _api-models: 2 | 3 | Models 4 | ====== 5 | 6 | .. automodule:: userena.models 7 | 8 | Return to :ref:`api`. 9 | 10 | upload_to_mugshot 11 | ----------------- 12 | 13 | .. autofunction:: userena.models.upload_to_mugshot 14 | 15 | UserenaSignup 16 | ------------- 17 | 18 | .. autoclass:: userena.models.UserenaSignup 19 | :members: 20 | 21 | UserenaBaseProfile 22 | ------------------ 23 | 24 | .. autoclass:: userena.models.UserenaBaseProfile 25 | :members: 26 | 27 | UserenaLanguageBaseProfile 28 | -------------------------- 29 | 30 | .. autoclass:: userena.models.UserenaLanguageBaseProfile 31 | :members: 32 | 33 | -------------------------------------------------------------------------------- /userena/templates/userena/email_change_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Email verification" %}{% endblock %} 5 | {% block content_title %}

{% trans 'Confirm your new email' %}

{% endblock %} 6 | 7 | {% block content %} 8 |

{% blocktrans with viewed_user.userena_signup.email_unconfirmed as email %}You have received an email at {{ email }}.{% endblocktrans %}

9 |

{% blocktrans with viewed_user.username as username %}To associate this email address with your account ({{ username }}) click on the link provided in this email.{% endblocktrans %}

10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_message_old.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% if not without_usernames %}{% blocktrans with user.username as username %}Dear {{ username }},{% endblocktrans %} 3 | {% endif %} 4 | {% blocktrans with site.name as site %}There was a request to change your email address at {{ site }}.{% endblocktrans %} 5 | 6 | {% blocktrans %}An email has been send to {{ new_email }} which contains a verification link. Click on the link in this email to activate it.{% endblocktrans %} 7 | 8 | {% trans "Thanks for using our site!" %} 9 | 10 | {% trans "Sincerely" %}, 11 | {{ site.name }} 12 | {% endautoescape %} 13 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_message_new.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% if not without_usernames %}{% blocktrans with user.username as username %}Dear {{ username }},{% endblocktrans %} 3 | {% endif %} 4 | {% blocktrans with site.name as site %}You requested a change of your email address at {{ site }}.{% endblocktrans %} 5 | 6 | 7 | {% trans "Please confirm this email address by clicking on the link below:" %} 8 | 9 | {{ protocol }}://{{ site.domain }}{% url 'userena_email_confirm' confirmation_key %} 10 | 11 | 12 | {% trans "Thanks for using our site!" %} 13 | 14 | {% trans "Sincerely" %}, 15 | {{ site.name }} 16 | {% endautoescape %} 17 | -------------------------------------------------------------------------------- /userena/templates/userena/activate_retry_success.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | 5 | {% block title %}{% trans "Account re-activation succeeded." %}{% endblock %} 6 | {% block content_title %}

{% trans "Account re-activation" %}

{% endblock %} 7 | 8 | {% block content %} 9 |

{% blocktrans %}You requested a new activation of your account..{% endblocktrans %}

10 |

{% blocktrans %}You have been sent an e-mail with an activation link to the supplied email.{% endblocktrans %}

11 |

{% blocktrans %}We will store your signup information for {{ userena_activation_days }} days on our server. {% endblocktrans %}

12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain about missing debug-only code: 12 | def __repr__ 13 | if self\.debug 14 | 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if 0: 21 | if __name__ == .__main__.: 22 | 23 | omit = 24 | */runtests/* 25 | */migrations/* 26 | */tests/* 27 | */south_migrations/* 28 | 29 | ignore_errors = True 30 | -------------------------------------------------------------------------------- /userena/templates/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | {% block title %}Userena{% endblock %} 8 | {% block meta %}{% endblock %} 9 | 10 | 11 |
12 | {% block body %} 13 |
14 | {% block content_title %}{% endblock %} 15 |
16 |
17 | {% block content %}{% endblock %} 18 |
19 | {% endblock %} 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templates/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | {% block title %}Userena{% endblock %} 8 | {% block meta %}{% endblock %} 9 | 10 | 11 |
12 | {% block body %} 13 |
14 | {% block content_title %}{% endblock %} 15 |
16 |
17 | {% block content %}{% endblock %} 18 |
19 | {% endblock %} 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/password_reset_message.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | {% blocktrans %}You're receiving this e-mail because you requested a password reset 3 | for your user account at {{ site_name }}{% endblocktrans %}. 4 | 5 | {% trans "Please go to the following page and choose a new password:" %} 6 | {% block reset_link %} 7 | {{ protocol }}://{{ domain }}{% url 'userena_password_reset_confirm' uid token %} 8 | {% endblock %} 9 | 10 | {% if not without_usernames %}{% blocktrans with user.username as username %} 11 | Your username, in case you've forgotten: {{ username }} 12 | {% endblocktrans %} 13 | {% endif %} 14 | {% trans "Thanks for using our site!" %} 15 | 16 | {% trans "Sincerely" %}, 17 | {{ site_name }} 18 | {% endautoescape %} 19 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/activation_email_message.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 | {% if not without_usernames %}

{% blocktrans with user.username as username %}Dear {{ username }},

{% endblocktrans %}{% endif %} 5 | {% blocktrans with site.name as site %}

Thank you for signing up at {{ site }}.

{% endblocktrans %} 6 |

7 | {% trans "To activate your account you should click on the link below:" %}
8 | {{ protocol }}://{{ site.domain }}{% url 'userena_activate' activation_key %} 9 |

10 |

11 | {% trans "Thanks for using our site!" %}
12 | {% trans "Sincerely" %},
13 | {{ site.name }} 14 |

15 | 16 | 17 | {% endautoescape %} 18 | -------------------------------------------------------------------------------- /userena/templates/userena/signup_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Signup almost done!" %}{% endblock %} 5 | 6 | {% block content_title %}

{% trans "Signup" %}

{% endblock %} 7 | 8 | {% block content %} 9 |

{% trans "Thank you for signing up with us!" %}

10 | 11 | {% if userena_activation_required %} 12 |

{% blocktrans %}You have been sent an e-mail with an activation link to the supplied email.{% endblocktrans %}

13 |

{% blocktrans %}We will store your signup information for {{ userena_activation_days }} days on our server. {% endblocktrans %}

14 | {% else %} 15 |

{% blocktrans %}You can now use the supplied credentials to signin.{% endblocktrans %}

16 | {% endif %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/emails/activation_email_message.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 |

Test message

5 | {% if not without_usernames %}

{% blocktrans with user.username as username %}Dear {{ username }},

{% endblocktrans %}{% endif %} 6 | {% blocktrans with site.name as site %}

Thank you for signing up at {{ site }}.

{% endblocktrans %} 7 |

8 | {% trans "To activate your account you should click on the link below:" %}
9 | {{ protocol }}://{{ site.domain }}{% url 'userena_activate' activation_key %} 10 |

11 |

12 | {% trans "Thanks for using our site!" %}
13 | {% trans "Sincerely" %},
14 | {{ site.name }} 15 |

16 | 17 | 18 | {% endautoescape %} 19 | -------------------------------------------------------------------------------- /docs/commands.rst: -------------------------------------------------------------------------------- 1 | .. _commands: 2 | 3 | Commands. 4 | ========= 5 | 6 | Userena currently comes with two commands. ``cleanexpired`` for cleaning out 7 | the expired users and ``check_permissions`` for checking the correct 8 | permissions needed by userena. 9 | 10 | Clean expired 11 | -------------- 12 | 13 | Search for users that still haven't verified their e-mail address after 14 | ``USERENA_ACTIVATION_DAYS`` and delete them. Run by :: 15 | 16 | ./manage.py clean_expired 17 | 18 | Check permissions 19 | ----------------- 20 | 21 | This command shouldn't be run as a cronjob. This is only for emergency 22 | situations when some permissions are not correctly set for users. For example 23 | when userena get's implemented in an already existing project. Run by :: 24 | 25 | ./manage.py check_permissions 26 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_message_old.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 | {% if not without_usernames %}

{% blocktrans with user.username as username %}Dear {{ username }},{% endblocktrans %}

{% endif %} 5 |

6 | {% blocktrans with site.name as site %}There was a request to change your email address at {{ site }}.{% endblocktrans %} 7 |

8 | 9 |

10 | {% blocktrans %}An email has been send to {{ new_email }} which contains a verification link. Click on the link in this email to activate it.{% endblocktrans %} 11 |

12 | 13 |

{% trans "Thanks for using our site!" %}

14 | 15 |

16 | {% trans "Sincerely" %},
17 | {{ site.name }} 18 |

19 | 20 | 21 | {% endautoescape %} 22 | -------------------------------------------------------------------------------- /demo/demo/templates/umessages/message_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'umessages/base_message.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

Conversation with {{ recipient }}

{% endblock %} 5 | 6 | {% block content %} 7 | {% trans "Reply" %} 8 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /docs/api/forms.rst: -------------------------------------------------------------------------------- 1 | .. _api-forms: 2 | 3 | Forms 4 | ===== 5 | 6 | .. automodule:: userena.forms 7 | 8 | Return to :ref:`api`. 9 | 10 | SignupForm 11 | ---------- 12 | 13 | .. autoclass:: userena.forms.SignupForm 14 | :members: 15 | 16 | SignupFormOnlyEmail 17 | ------------------- 18 | 19 | .. autoclass:: userena.forms.SignupFormOnlyEmail 20 | :members: 21 | 22 | SignupFormTos 23 | ------------- 24 | 25 | .. autoclass:: userena.forms.SignupFormTos 26 | :members: 27 | 28 | AuthenticationForm 29 | ------------------ 30 | 31 | .. autoclass:: userena.forms.AuthenticationForm 32 | :members: 33 | 34 | ChangeEmailForm 35 | --------------- 36 | 37 | .. autoclass:: userena.forms.ChangeEmailForm 38 | :members: 39 | 40 | EditProfileForm 41 | --------------- 42 | 43 | .. autoclass:: userena.forms.EditProfileForm 44 | :members: 45 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/confirmation_email_message_new.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 | {% if not without_usernames %}

{% blocktrans with user.username as username %}Dear {{ username }},

{% endblocktrans %}{% endif %} 5 |

6 | {% blocktrans with site.name as site %}You requested a change of your email address at {{ site }}.{% endblocktrans %} 7 |

8 |

9 | {% trans "Please confirm this email address by clicking on the link below:" %}
10 | {{ protocol }}://{{ site.domain }}{% url 'userena_email_confirm' confirmation_key %} 11 |

12 |

13 | {% trans "Thanks for using our site!" %}
14 | {% trans "Sincerely" %},
15 | {{ site_name }} 16 |

17 | 18 | 19 | {% endautoescape %} 20 | -------------------------------------------------------------------------------- /userena/templates/userena/signup_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Signup" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | {% csrf_token %} 9 |
10 | {% trans "Signup" %} 11 | {{ form.non_field_errors }} 12 | {% for field in form %} 13 | {{ field.errors }} 14 | {# Displaying checkboxes differently #} 15 | {% if field.name == 'tos' %} 16 |

17 | 18 |

19 | {% else %} 20 |

21 | {{ field.label_tag }} 22 | {{ field }} 23 |

24 | {% endif %} 25 | {% endfor %} 26 |
27 | 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /userena/templates/userena/invite_new_user.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Invite New User" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | {% csrf_token %} 9 |
10 | {% trans "Invite New User" %} 11 | {{ form.non_field_errors }} 12 | {% for field in form %} 13 | {{ field.errors }} 14 | {# Displaying checkboxes differently #} 15 | {% if field.name == 'tos' %} 16 |

17 | 18 |

19 | {% else %} 20 |

21 | {{ field.label_tag }} 22 | {{ field }} 23 |

24 | {% endif %} 25 | {% endfor %} 26 |
27 | 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /userena/templates/userena/emails/password_reset_message.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% autoescape off %} 2 | 3 | 4 |

5 | {% blocktrans %}You're receiving this e-mail because you requested a password reset 6 | for your user account at {{ site_name }}{% endblocktrans %}. 7 |

8 |

9 | {% trans "Please go to the following page and choose a new password:" %}
10 | {% block reset_link %} 11 | {{ protocol }}://{{ domain }}{% url 'userena_password_reset_confirm' uid token %} 12 | {% endblock %} 13 |

14 | {% if not without_usernames %} 15 |

{% blocktrans with user.username as username %}Your username, in case you've forgotten: {{ username }}{% endblocktrans %}

16 | {% endif %} 17 |

18 | {% trans "Thanks for using our site!" %}
19 | {% trans "Sincerely" %},
20 | {{ site_name }} 21 |

22 | 23 | 24 | {% endautoescape %} 25 | -------------------------------------------------------------------------------- /userena/tests/tests_urls.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse, NoReverseMatch 3 | 4 | 5 | class UserenaUrlsTests(TestCase): 6 | """ Test url resolve """ 7 | 8 | def test_resolve_email_with_plus_url(self): 9 | username = 'foo+bar@example.com' 10 | test_urls = [ 11 | 'userena_signup_complete', 12 | 'userena_email_change', 13 | 'userena_email_change_complete', 14 | 'userena_email_confirm_complete', 15 | 'userena_disabled', 16 | 'userena_password_change', 17 | 'userena_password_change_complete', 18 | 'userena_profile_edit', 19 | 'userena_profile_detail', 20 | ] 21 | for url_name in test_urls: 22 | try: 23 | reverse(url_name, kwargs={'username': username}) 24 | except NoReverseMatch as ex: 25 | self.failed(ex) 26 | -------------------------------------------------------------------------------- /userena/migrations/0002_auto_20170214_1529.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-14 11:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import userena.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('userena', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelManagers( 17 | name='userenasignup', 18 | managers=[ 19 | ('objects', userena.managers.UserenaManager()), 20 | ], 21 | ), 22 | migrations.AlterField( 23 | model_name='userenasignup', 24 | name='email_unconfirmed', 25 | field=models.EmailField(blank=True, help_text='Temporary email address when the user requests an email change.', max_length=254, verbose_name='unconfirmed email address'), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /userena/migrations/0003_auto_20170217_1640.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-02-17 13:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('userena', '0002_auto_20170214_1529'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='userenasignup', 17 | name='invitation_key', 18 | field=models.CharField(blank=True, max_length=40, verbose_name='invitation key'), 19 | ), 20 | migrations.AddField( 21 | model_name='userenasignup', 22 | name='invitation_status', 23 | field=models.CharField(choices=[('INV', 'Invitation Mail was sent'), ('PSWRST', 'Password was reset by user'), ('PRFEDIT', 'Profile was edited by user')], default='INV', max_length=7), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /userena/tests/tests_decorators.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.urls import reverse 3 | 4 | from userena.utils import get_gravatar 5 | from userena import settings as userena_settings 6 | 7 | import re 8 | 9 | class DecoratorTests(TestCase): 10 | """ Test the decorators """ 11 | 12 | def test_secure_required(self): 13 | """ 14 | Test that the ``secure_required`` decorator does a permanent redirect 15 | to a secured page. 16 | 17 | """ 18 | with self.settings(USERENA_USE_HTTPS=True): 19 | response = self.client.get(reverse('userena_signin')) 20 | 21 | # Test for the permanent redirect 22 | self.assertEqual(response.status_code, 301) 23 | 24 | # Test if the redirected url contains 'https'. Couldn't use 25 | # ``assertRedirects`` here because the redirected to page is 26 | # non-existant. 27 | self.assertTrue('https' in response.get('Location')) 28 | -------------------------------------------------------------------------------- /userena/runtests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.conf import settings 3 | 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.conf.urls.static import static 6 | 7 | from django.contrib import admin 8 | admin.autodiscover() 9 | 10 | urlpatterns = [ 11 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 12 | url(r'^admin/', include(admin.site.urls)), 13 | 14 | # Demo Override the signup form with our own, which includes a 15 | # first and last name. 16 | # (r'^accounts/signup/$', 17 | # 'userena.views.signup', 18 | # {'signup_form': SignupFormExtra}), 19 | 20 | url(r'^accounts/', include('userena.urls')), 21 | url(r'^messages/', include('userena.contrib.umessages.urls')), 22 | url(r'^i18n/', include('django.conf.urls.i18n')), 23 | ] 24 | 25 | # Add media and static files 26 | urlpatterns += staticfiles_urlpatterns() 27 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 28 | 29 | 30 | -------------------------------------------------------------------------------- /userena/templates/userena/email_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% blocktrans with user.username as username %}Account » {{ username }}{% endblocktrans %}

{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 14 | {% csrf_token %} 15 |
16 | {% trans "Change email address" %} 17 | {{ form.as_p }} 18 |
19 | 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /docs/api/views.rst: -------------------------------------------------------------------------------- 1 | .. _api-views: 2 | 3 | Views 4 | ===== 5 | 6 | .. automodule:: userena.views 7 | 8 | signup 9 | ------ 10 | 11 | .. autofunction:: userena.views.signup 12 | 13 | activate 14 | -------- 15 | 16 | .. autofunction:: userena.views.activate 17 | 18 | email_confirm 19 | ------------- 20 | 21 | .. autofunction:: userena.views.email_confirm 22 | 23 | direct_to_user_template 24 | ----------------------- 25 | 26 | .. autofunction:: userena.views.direct_to_user_template 27 | 28 | signin 29 | ------ 30 | 31 | .. autofunction:: userena.views.signin 32 | 33 | email_change 34 | ------------ 35 | 36 | .. autofunction:: userena.views.email_change 37 | 38 | password_change 39 | --------------- 40 | 41 | .. autofunction:: userena.views.password_change 42 | 43 | profile_edit 44 | ------------ 45 | 46 | .. autofunction:: userena.views.profile_edit 47 | 48 | profile_detail 49 | -------------- 50 | 51 | .. autofunction:: userena.views.profile_detail 52 | 53 | profile_list 54 | ------------ 55 | 56 | .. autofunction:: userena.views.profile_list 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Userena 2 | 3 | [![Build Status](https://travis-ci.org/bread-and-pepper/django-userena.svg)](https://travis-ci.org/bread-and-pepper/django-userena) 4 | [![Coverage Status](https://img.shields.io/coveralls/bread-and-pepper/django-userena.svg)](https://coveralls.io/r/bread-and-pepper/django-userena) 5 | 6 | Userena is a Django application that supplies your Django project with full 7 | account management. It's a fully customizable application that takes care of 8 | the signup, activation, messaging and more. It's BSD licensed, which means you 9 | can use it commercially for free! 10 | 11 | ## [Documentation](http://docs.django-userena.org/en/latest/index.html) 12 | 13 | Complete documentation about the 14 | [installation](http://docs.django-userena.org/en/latest/installation.html), 15 | [settings](http://docs.django-userena.org/en/latest/settings.html) and 16 | [F.A.Q.](http://docs.django-userena.org/en/latest/faq.html) is available on 17 | [Read the Docs](http://docs.django-userena.org/en/latest/index.html). 18 | 19 | For list of updates and changes see `UPDATES.md` file. 20 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url, include 2 | from django.conf import settings 3 | 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.conf.urls.static import static 6 | 7 | from django.contrib import admin 8 | admin.autodiscover() 9 | 10 | urlpatterns = [ 11 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 12 | url(r'^admin/', include(admin.site.urls)), 13 | 14 | # Demo Override the signup form with our own, which includes a 15 | # first and last name. 16 | # (r'^accounts/signup/$', 17 | # 'userena.views.signup', 18 | # {'signup_form': SignupFormExtra}), 19 | 20 | url(r'^accounts/', include('userena.urls')), 21 | url(r'^messages/', include('userena.contrib.umessages.urls')), 22 | url(r'^$', 'profiles.views.promo', name='promo'), 23 | url(r'^i18n/', include('django.conf.urls.i18n')), 24 | ] 25 | 26 | # Add media and static files 27 | urlpatterns += staticfiles_urlpatterns() 28 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 29 | 30 | 31 | -------------------------------------------------------------------------------- /userena/templates/userena/signin_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Signin" %}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | {% csrf_token %} 9 |
10 | {% trans "Signin" %} 11 | {{ form.non_field_errors }} 12 | {% for field in form %} 13 | {{ field.errors }} 14 | {# Displaying checkboxes differently #} 15 | {% if field.name == 'remember_me' %} 16 |

17 | 18 |

19 | {% else %} 20 |

21 | {{ field.label_tag }} 22 | {{ field }} 23 |

24 | {% endif %} 25 | {% endfor %} 26 |
27 | 28 |

{% trans "Forgot your password?" %}

29 | {% if next %}{% endif %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /userena/templates/userena/password_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Change password" %}{% endblock %} 5 | 6 | {% block content_title %}

{% blocktrans with user.username as username %}Account » {{ username }}{% endblocktrans %}

{% endblock %} 7 | 8 | {% block content %} 9 |
10 | 16 |
17 | {% trans "Change Password" %} 18 | {% csrf_token %} 19 | {{ form.as_p }} 20 |
21 | 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /userena/templates/userena/profile_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account setup" %}{% endblock %} 5 | 6 | {% block content_title %}

{% blocktrans with profile.user.username as username %}Account » {{ username }}{% endblocktrans %}

{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
11 | 17 | {% csrf_token %} 18 |
19 | {% trans "Edit Profile" %} 20 | {{ form.as_p }} 21 |
22 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /userena/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.admin import UserAdmin 4 | from django.utils.translation import ugettext as _ 5 | from guardian.admin import GuardedModelAdmin 6 | 7 | from userena.models import UserenaSignup 8 | from userena import settings as userena_settings 9 | from userena.utils import get_profile_model 10 | 11 | class UserenaSignupInline(admin.StackedInline): 12 | model = UserenaSignup 13 | max_num = 1 14 | 15 | class UserenaAdmin(UserAdmin, GuardedModelAdmin): 16 | inlines = [UserenaSignupInline, ] 17 | list_display = ('username', 'email', 'first_name', 'last_name', 18 | 'is_staff', 'is_active', 'date_joined') 19 | list_filter = ('is_staff', 'is_superuser', 'is_active') 20 | 21 | 22 | if userena_settings.USERENA_REGISTER_USER: 23 | try: 24 | admin.site.unregister(get_user_model()) 25 | except admin.sites.NotRegistered: 26 | pass 27 | 28 | admin.site.register(get_user_model(), UserenaAdmin) 29 | 30 | if userena_settings.USERENA_REGISTER_PROFILE: 31 | admin.site.register(get_profile_model(), GuardedModelAdmin) 32 | -------------------------------------------------------------------------------- /docs/signals.rst: -------------------------------------------------------------------------------- 1 | .. _signals: 2 | 3 | Signals 4 | ======= 5 | 6 | Userena contains a few signals which you can use in your own application if 7 | you want to have custom actions when a account get's changed. All signals are 8 | located in ``userena/signals.py`` file. 9 | 10 | signup_complete 11 | --------------- 12 | 13 | This signal get's fired when an user signs up at your site. Note: This doesn't 14 | mean that the user is activated. The signal provides you with the ``user`` 15 | argument which Django's :class:`User` class. 16 | 17 | activation_complete 18 | ------------------- 19 | 20 | A user has succesfully activated their account. The signal provides you with 21 | the ``user`` argument which Django's :class:`User` class. 22 | 23 | confirmation_complete 24 | --------------------- 25 | 26 | A user has succesfully changed their email. The signal provides you 27 | with the ``user`` argument which Django's :class:`User` class, and the 28 | ``old_email`` argument which is the user's old email address as a 29 | string. 30 | 31 | password_complete 32 | ----------------- 33 | 34 | A user has succesfully changed their password. The signal provides you with 35 | the ``user`` argument which Django's :class:`User` class. 36 | -------------------------------------------------------------------------------- /userena/contrib/umessages/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import gettext_lazy as _ 3 | from django.contrib import admin 4 | from django.contrib.auth.models import Group 5 | 6 | from userena.contrib.umessages.models import Message, MessageContact, MessageRecipient 7 | 8 | class MessageRecipientInline(admin.TabularInline): 9 | """ Inline message recipients """ 10 | model = MessageRecipient 11 | 12 | class MessageAdmin(admin.ModelAdmin): 13 | """ Admin message class with inline recipients """ 14 | inlines = [ 15 | MessageRecipientInline, 16 | ] 17 | 18 | fieldsets = ( 19 | (None, { 20 | 'fields': ( 21 | 'sender', 'body', 22 | ), 23 | 'classes': ('monospace' ), 24 | }), 25 | (_('Date/time'), { 26 | 'fields': ( 27 | 'sender_deleted_at', 28 | ), 29 | 'classes': ('collapse', 'wide'), 30 | }), 31 | ) 32 | list_display = ('sender', 'body', 'sent_at') 33 | list_filter = ('sent_at', 'sender') 34 | search_fields = ('body',) 35 | 36 | admin.site.register(Message, MessageAdmin) 37 | admin.site.register(MessageContact) 38 | -------------------------------------------------------------------------------- /userena/contrib/umessages/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from userena.contrib.umessages import views as messages_views 3 | from django.contrib.auth.decorators import login_required 4 | 5 | urlpatterns = [ 6 | url(r'^compose/$', 7 | messages_views.message_compose, 8 | name='userena_umessages_compose'), 9 | 10 | url(r'^compose/(?P[\+\.\w]+)/$', 11 | messages_views.message_compose, 12 | name='userena_umessages_compose_to'), 13 | 14 | url(r'^reply/(?P[\d]+)/$', 15 | messages_views.message_compose, 16 | name='userena_umessages_reply'), 17 | 18 | url(r'^view/(?P[\.\w]+)/$', 19 | login_required(messages_views.MessageDetailListView.as_view()), 20 | name='userena_umessages_detail'), 21 | 22 | url(r'^remove/$', 23 | messages_views.message_remove, 24 | name='userena_umessages_remove'), 25 | 26 | url(r'^unremove/$', 27 | messages_views.message_remove, 28 | {'undo': True}, 29 | name='userena_umessages_unremove'), 30 | 31 | url(r'^$', 32 | login_required(messages_views.MessageListView.as_view()), 33 | name='userena_umessages_list'), 34 | ] 35 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templates/umessages/message_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'umessages/base_message.html' %} 2 | {% load i18n umessages_tags %} 3 | 4 | {% block content_title %}

{% trans "Messages" %}

{% endblock %} 5 | 6 | {% block content %} 7 | {% get_unread_message_count_for user as unread_message_count %} 8 | {% blocktrans %}{{ unread_message_count }} new messages.{% endblocktrans %} 9 | {% trans "Compose" %} 10 |
    11 | {% for message in message_list %} 12 |
  • 13 | {% if message.um_from_user == user %} 14 | {{ message.um_to_user }} 15 | {% get_unread_message_count_between user and message.um_to_user as unread_between_count %} 16 | {% else %} 17 | {{ message.um_from_user }} 18 | {% get_unread_message_count_between user and message.um_from_user as unread_between_count %} 19 | {% endif %} 20 | {% blocktrans with message.latest_message as latest_message %}{{ latest_message }} ({{ unread_between_count }} new){% endblocktrans %} 21 |
  • 22 | {% endfor %} 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /userena/contrib/umessages/fixtures/messages.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "umessages.message", 5 | "fields": { 6 | "body": "Hello from your friend", 7 | "sender_deleted_at": null, 8 | "sent_at": "2010-01-01T12:00:00.019Z", 9 | "sender": 1 10 | } 11 | }, 12 | { 13 | "pk": 2, 14 | "model": "umessages.message", 15 | "fields": { 16 | "body": "Hello from your mother", 17 | "sender_deleted_at": "2010-01-01T12:00:00.019Z", 18 | "sent_at": "2010-01-01T12:00:00.019Z", 19 | "sender": 2 20 | } 21 | }, 22 | { 23 | "pk": 1, 24 | "model": "umessages.messagerecipient", 25 | "fields": { 26 | "message": 1, 27 | "read_at": null, 28 | "deleted_at": null, 29 | "user": 2 30 | } 31 | }, 32 | { 33 | "pk": 2, 34 | "model": "umessages.messagerecipient", 35 | "fields": { 36 | "message": 2, 37 | "read_at": "2010-01-01T12:00:00.019Z", 38 | "deleted_at": null, 39 | "user": 1 40 | } 41 | }, 42 | { 43 | "pk": 1, 44 | "model": "umessages.messagecontact", 45 | "fields": { 46 | "um_from_user": 1, 47 | "um_to_user": 2, 48 | "latest_message": 2 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo_2 project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo_2.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /userena/contrib/umessages/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from userena.contrib.umessages.fields import CommaSeparatedUserField 5 | from userena.contrib.umessages.models import Message, MessageRecipient 6 | 7 | import datetime 8 | 9 | class ComposeForm(forms.Form): 10 | to = CommaSeparatedUserField(label=_("To")) 11 | body = forms.CharField(label=_("Message"), 12 | widget=forms.Textarea({'class': 'message'}), 13 | required=True) 14 | 15 | def save(self, sender): 16 | """ 17 | Save the message and send it out into the wide world. 18 | 19 | :param sender: 20 | The :class:`User` that sends the message. 21 | 22 | :param parent_msg: 23 | The :class:`Message` that preceded this message in the thread. 24 | 25 | :return: The saved :class:`Message`. 26 | 27 | """ 28 | um_to_user_list = self.cleaned_data['to'] 29 | body = self.cleaned_data['body'] 30 | 31 | msg = Message.objects.send_message(sender, 32 | um_to_user_list, 33 | body) 34 | 35 | return msg 36 | -------------------------------------------------------------------------------- /demo/demo/static/css/reset.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | 3 | 4 | html, body, div, span, applet, object, iframe, 5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 6 | a, abbr, acronym, address, big, cite, code, 7 | del, dfn, em, font, img, ins, kbd, q, s, samp, 8 | small, strike, strong, sub, sup, tt, var, 9 | b, u, i, center, 10 | dl, dt, dd, ol, ul, li, 11 | fieldset, form, label, legend, 12 | table, caption, tbody, tfoot, thead, tr, th, td { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | outline: 0; 17 | font-size: 100%; 18 | vertical-align: baseline; 19 | background: transparent; 20 | } 21 | body { 22 | line-height: 1; 23 | } 24 | ol, ul { 25 | list-style: none; 26 | } 27 | blockquote, q { 28 | quotes: none; 29 | } 30 | blockquote:before, blockquote:after, 31 | q:before, q:after { 32 | content: ''; 33 | content: none; 34 | } 35 | 36 | /* remember to define focus styles! */ 37 | :focus { 38 | outline: 0; 39 | } 40 | 41 | /* remember to highlight inserts somehow! */ 42 | ins { 43 | text-decoration: none; 44 | } 45 | del { 46 | text-decoration: line-through; 47 | } 48 | 49 | /* tables still need 'cellspacing="0"' in the markup */ 50 | table { 51 | border-collapse: collapse; 52 | border-spacing: 0; 53 | } 54 | 55 | a:focus, a:active{ 56 | outline: none; 57 | } -------------------------------------------------------------------------------- /userena/templates/userena/profile_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% trans 'Profiles' %}

{% endblock %} 5 | 6 | {% block content %} 7 | 16 | 17 | {% if is_paginated %} 18 | 35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/profile_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% trans 'Profiles' %}

{% endblock %} 5 | 6 | {% block content %} 7 | 16 | 17 | {% if is_paginated %} 18 | 33 | {% endif %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /userena/decorators.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponsePermanentRedirect 2 | from django.utils.decorators import available_attrs 3 | from django.conf import settings 4 | from django.utils.functional import wraps 5 | 6 | from userena import settings as userena_settings 7 | 8 | 9 | def secure_required(view_func): 10 | """ 11 | Decorator to switch an url from http to https. 12 | 13 | If a view is accessed through http and this decorator is applied to that 14 | view, than it will return a permanent redirect to the secure (https) 15 | version of the same view. 16 | 17 | The decorator also must check that ``USERENA_USE_HTTPS`` is enabled. If 18 | disabled, it should not redirect to https because the project doesn't 19 | support it. 20 | 21 | """ 22 | def _wrapped_view(request, *args, **kwargs): 23 | if not request.is_secure(): 24 | if getattr(settings, 'USERENA_USE_HTTPS', userena_settings.DEFAULT_USERENA_USE_HTTPS): 25 | request_url = request.build_absolute_uri(request.get_full_path()) 26 | secure_url = request_url.replace('http://', 'https://') 27 | return HttpResponsePermanentRedirect(secure_url) 28 | return view_func(request, *args, **kwargs) 29 | return wraps(view_func, assigned=available_attrs(view_func))(_wrapped_view) 30 | -------------------------------------------------------------------------------- /demo/demo/templates/umessages/message_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'umessages/base_message.html' %} 2 | {% load i18n umessages_tags %} 3 | 4 | {% block content_title %}{% get_unread_message_count_for user as unread_message_count %} 5 |

Messages ({{ unread_message_count }} )

{% endblock %} 6 | 7 | {% block content %} 8 | 9 | + Compose message 10 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 2.7 3 | sudo: false 4 | 5 | # note: python3.5 is not preinstalled on docker-based (i.e. sudo: false) 6 | # Travis workers at the moment of this writing, so we need to get 7 | # it via addon repository. 8 | addons: 9 | apt: 10 | sources: 11 | - deadsnakes 12 | packages: 13 | - python3.5 14 | - python3.5-dev 15 | 16 | env: 17 | - TOX_ENV=py26-django15 18 | - TOX_ENV=py26-django16 19 | 20 | - TOX_ENV=py27-django15 21 | - TOX_ENV=py27-django16 22 | - TOX_ENV=py27-django17 23 | - TOX_ENV=py27-django18 24 | - TOX_ENV=py27-django19 25 | 26 | - TOX_ENV=py32-django15 27 | - TOX_ENV=py32-django16 28 | - TOX_ENV=py32-django17 29 | - TOX_ENV=py32-django18 30 | 31 | - TOX_ENV=py33-django15 32 | - TOX_ENV=py33-django16 33 | - TOX_ENV=py33-django17 34 | - TOX_ENV=py33-django18 35 | 36 | - TOX_ENV=py34-django17 37 | - TOX_ENV=py34-django18 38 | - TOX_ENV=py34-django19 39 | 40 | - TOX_ENV=py35-django18 41 | - TOX_ENV=py35-django19 42 | 43 | - TOX_ENV=coverage 44 | install: 45 | # note: pip>=8.0.0 does not work on Python 3.2 but we want to test on py32 46 | # note: virtualenv>=14.0.0 bundles newer version of pip that does not work on 47 | # py32 48 | - pip install -U 'tox==2.2.1' 'pip<8.0.0' 'virtualenv<14.0.0' 49 | script: 50 | - tox -e $TOX_ENV 51 | -------------------------------------------------------------------------------- /userena/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils import translation 2 | from django.core.exceptions import ObjectDoesNotExist 3 | from django.conf import settings 4 | 5 | from userena import settings as userena_settings 6 | from userena.compat import SiteProfileNotAvailable 7 | from userena.utils import get_user_profile 8 | 9 | 10 | class UserenaLocaleMiddleware(object): 11 | """ 12 | Set the language by looking at the language setting in the profile. 13 | 14 | It doesn't override the cookie that is set by Django so a user can still 15 | switch languages depending if the cookie is set. 16 | 17 | """ 18 | def process_request(self, request): 19 | lang_cookie = request.session.get(settings.LANGUAGE_COOKIE_NAME) 20 | if not lang_cookie: 21 | if request.user.is_authenticated(): 22 | try: 23 | profile = get_user_profile(user=request.user) 24 | except (ObjectDoesNotExist, SiteProfileNotAvailable): 25 | profile = False 26 | 27 | if profile: 28 | try: 29 | lang = getattr(profile, userena_settings.USERENA_LANGUAGE_FIELD) 30 | translation.activate(lang) 31 | request.LANGUAGE_CODE = translation.get_language() 32 | except AttributeError: pass 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | ; py26 support was dropped in django1.7 4 | py26-django{15,16}, 5 | ; py27 still has the widest django support 6 | py27-django{15,16,17,18,19}, 7 | ; py32, py33 support was officially introduced in django1.5 8 | ; py32, py33 support was dropped in django1.9 9 | py32-django{15,16,17,18}, 10 | py33-django{15,16,17,18}, 11 | ; py34 support was officially introduced in django1.7 12 | py34-django{17,18,19} 13 | ; py35 support was officially introduced in django1.8 14 | py35-django{18,19} 15 | 16 | [testenv] 17 | usedevelop = True 18 | deps = 19 | django{15,16}: south 20 | django{15,16}: django-guardian<1.4.0 21 | django15: django==1.5.12 22 | django16: django==1.6.11 23 | django17: django==1.7.11 24 | django18: django==1.8.7 25 | django19: django==1.9 26 | coverage: django==1.9 27 | coverage: coverage==4.0.3 28 | coverage: coveralls==1.1 29 | 30 | basepython = 31 | py35: python3.5 32 | py34: python3.4 33 | py33: python3.3 34 | py32: python3.2 35 | py27: python2.7 36 | py26: python2.6 37 | 38 | commands={envpython} userena/runtests/runtests.py userena umessages {posargs} 39 | 40 | [testenv:coverage] 41 | basepython = python2.7 42 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 43 | commands= 44 | coverage run --source=userena userena/runtests/runtests.py userena umessages {posargs} 45 | coveralls 46 | -------------------------------------------------------------------------------- /userena/templates/userena/list_invited_users.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

{% trans 'Invited Users' %}

{% endblock %} 5 | 6 | {% block content %} 7 |

Number of Users you could add.

8 |

{{numOfRemainingInvitationTicket}}

9 | 19 | 20 | {% comment %} 21 | {% if is_paginated %} 22 | 39 | {% endif %} 40 | {% endcomment %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /userena/tests/profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from userena.models import UserenaBaseProfile 5 | from userena.utils import user_model_label 6 | 7 | import datetime 8 | 9 | class Profile(UserenaBaseProfile): 10 | """ Default profile """ 11 | GENDER_CHOICES = ( 12 | (1, _('Male')), 13 | (2, _('Female')), 14 | ) 15 | 16 | user = models.OneToOneField(user_model_label, 17 | unique=True, 18 | verbose_name=_('user'), 19 | related_name='profile') 20 | 21 | gender = models.PositiveSmallIntegerField(_('gender'), 22 | choices=GENDER_CHOICES, 23 | blank=True, 24 | null=True) 25 | website = models.URLField(_('website'), blank=True) 26 | location = models.CharField(_('location'), max_length=255, blank=True) 27 | about_me = models.TextField(_('about me'), blank=True) 28 | language = models.TextField(_('language'), blank=True) 29 | 30 | class SecondProfile(UserenaBaseProfile): 31 | user = models.OneToOneField(user_model_label, 32 | unique=True, 33 | verbose_name=_('user'), 34 | related_name='profile_second') 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Petar Radosevic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/email_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block content_title %}

Account » {{ account.user.username }}

{% endblock %} 5 | 6 | {% block content %} 7 | 8 | 14 | 15 |
16 | {% if user.username == account.user.username %} 17 | 23 | {% endif %} 24 | {% csrf_token %} 25 |
26 | {% trans "Change email address" %} 27 | {{ form.as_p }} 28 |
29 | 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/password_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Change password" %}{% endblock %} 5 | 6 | {% block content_title %}

Account » {{ account.user.username }}

{% endblock %} 7 | 8 | {% block content %} 9 | 10 | 16 | 17 |
18 | {% if user.username == account.user.username %} 19 | 25 | {% endif %} 26 |
27 | {% trans "Change Password" %} 28 | {% csrf_token %} 29 | {{ form.as_p }} 30 |
31 | 32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/profile_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% trans "Account setup" %}{% endblock %} 5 | 6 | {% block content_title %}

Account » {{ account.user.username }}

{% endblock %} 7 | 8 | {% block content %} 9 | 10 | 11 | 17 | 18 | 19 |
20 | {% if user.username == account.user.username %} 21 | 27 | {% endif %} 28 | {% csrf_token %} 29 |
30 | {% trans "Edit Profile" %} 31 | {{ form.as_p }} 32 |
33 | 34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /demo/profiles/forms.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django import forms 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from userena.forms import SignupForm 8 | 9 | class SignupFormExtra(SignupForm): 10 | """ 11 | A form to demonstrate how to add extra fields to the signup form, in this 12 | case adding the first and last name. 13 | 14 | 15 | """ 16 | first_name = forms.CharField(label=_('First name'), 17 | max_length=30, 18 | required=False) 19 | 20 | last_name = forms.CharField(label=_('Last name'), 21 | max_length=30, 22 | required=False) 23 | 24 | def __init__(self, *args, **kw): 25 | """ 26 | 27 | A bit of hackery to get the first name and last name at the top of the 28 | form instead at the end. 29 | 30 | """ 31 | super(SignupFormExtra, self).__init__(*args, **kw) 32 | # Put the first and last name at the top 33 | new_order = self.fields.keyOrder[:-2] 34 | new_order.insert(0, 'first_name') 35 | new_order.insert(1, 'last_name') 36 | self.fields.keyOrder = new_order 37 | 38 | def save(self): 39 | """ 40 | Override the save method to save the first and last name to the user 41 | field. 42 | 43 | """ 44 | # First save the parent form and get the user. 45 | new_user = super(SignupFormExtra, self).save() 46 | 47 | new_user.first_name = self.cleaned_data['first_name'] 48 | new_user.last_name = self.cleaned_data['last_name'] 49 | new_user.save() 50 | 51 | # Userena expects to get the new user from this form, so return the new 52 | # user. 53 | return new_user 54 | -------------------------------------------------------------------------------- /demo/profiles/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils.translation import ugettext_lazy as _ 3 | 4 | from userena.models import UserenaLanguageBaseProfile 5 | from userena.utils import user_model_label 6 | 7 | import datetime 8 | 9 | class Profile(UserenaLanguageBaseProfile): 10 | """ Default profile """ 11 | GENDER_CHOICES = ( 12 | (1, _('Male')), 13 | (2, _('Female')), 14 | ) 15 | 16 | user = models.OneToOneField(user_model_label, 17 | unique=True, 18 | verbose_name=_('user'), 19 | related_name='profile') 20 | 21 | gender = models.PositiveSmallIntegerField(_('gender'), 22 | choices=GENDER_CHOICES, 23 | blank=True, 24 | null=True) 25 | website = models.URLField(_('website'), blank=True) 26 | location = models.CharField(_('location'), max_length=255, blank=True) 27 | birth_date = models.DateField(_('birth date'), blank=True, null=True) 28 | about_me = models.TextField(_('about me'), blank=True) 29 | 30 | @property 31 | def age(self): 32 | if not self.birth_date: return False 33 | else: 34 | today = datetime.date.today() 35 | # Raised when birth date is February 29 and the current year is not a 36 | # leap year. 37 | try: 38 | birthday = self.birth_date.replace(year=today.year) 39 | except ValueError: 40 | day = today.day - 1 if today.day != 1 else today.day + 2 41 | birthday = self.birth_date.replace(year=today.year, day=day) 42 | if birthday > today: return today.year - self.birth_date.year - 1 43 | else: return today.year - self.birth_date.year 44 | -------------------------------------------------------------------------------- /userena/contrib/umessages/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib.auth import get_user_model 5 | from django.test import TestCase 6 | 7 | from userena.contrib.umessages.forms import ComposeForm 8 | 9 | 10 | class ComposeFormTests(TestCase): 11 | """ Test the compose form. """ 12 | fixtures = ['users'] 13 | 14 | def test_invalid_data(self): 15 | """ 16 | Test the save method of :class:`ComposeForm` 17 | 18 | We don't need to make the ``to`` field sweat because we have done that 19 | in the ``fields`` test. 20 | 21 | """ 22 | invalid_data_dicts = [ 23 | # No body 24 | {'data': {'to': 'john', 25 | 'body': ''}, 26 | 'error': ('body', ['This field is required.'])}, 27 | ] 28 | 29 | for invalid_dict in invalid_data_dicts: 30 | form = ComposeForm(data=invalid_dict['data']) 31 | self.assertFalse(form.is_valid()) 32 | self.assertEqual(form.errors[invalid_dict['error'][0]], 33 | invalid_dict['error'][1]) 34 | 35 | def test_save_msg(self): 36 | """ Test valid data """ 37 | valid_data = {'to': 'john, jane', 38 | 'body': 'Body'} 39 | 40 | form = ComposeForm(data=valid_data) 41 | 42 | self.assertTrue(form.is_valid()) 43 | 44 | # Save the form. 45 | sender = get_user_model().objects.get(username='jane') 46 | msg = form.save(sender) 47 | 48 | # Check if the values are set correctly 49 | self.assertEqual(msg.body, valid_data['body']) 50 | self.assertEqual(msg.sender, sender) 51 | self.assertTrue(msg.sent_at) 52 | 53 | # Check recipients 54 | self.assertEqual(msg.recipients.all()[0].username, 'jane') 55 | self.assertEqual(msg.recipients.all()[1].username, 'john') 56 | -------------------------------------------------------------------------------- /userena/backends.py: -------------------------------------------------------------------------------- 1 | import django.core.validators 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.backends import ModelBackend 4 | 5 | 6 | class UserenaAuthenticationBackend(ModelBackend): 7 | """ 8 | Custom backend because the user must be able to supply a ``email`` or 9 | ``username`` to the login form. 10 | 11 | """ 12 | def authenticate(self, identification, password=None, check_password=True): 13 | """ 14 | Authenticates a user through the combination email/username with 15 | password. 16 | 17 | :param identification: 18 | A string containing the username or e-mail of the user that is 19 | trying to authenticate. 20 | 21 | :password: 22 | Optional string containing the password for the user. 23 | 24 | :param check_password: 25 | Boolean that defines if the password should be checked for this 26 | user. Always keep this ``True``. This is only used by userena at 27 | activation when a user opens a page with a secret hash. 28 | 29 | :return: The signed in :class:`User`. 30 | 31 | """ 32 | User = get_user_model() 33 | try: 34 | django.core.validators.validate_email(identification) 35 | try: user = User.objects.get(email__iexact=identification) 36 | except User.DoesNotExist: return None 37 | except django.core.validators.ValidationError: 38 | try: user = User.objects.get(username__iexact=identification) 39 | except User.DoesNotExist: return None 40 | if check_password: 41 | if user.check_password(password): 42 | return user 43 | return None 44 | else: return user 45 | 46 | def get_user(self, user_id): 47 | User = get_user_model() 48 | try: return User.objects.get(pk=user_id) 49 | except User.DoesNotExist: 50 | return None 51 | -------------------------------------------------------------------------------- /demo/demo/templates/userena/profile_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{{ profile.user.username }}'s {% trans "profile" %}.{% endblock %} 5 | {% block content_title %}

{{ profile.user.username }} {% if profile.user.get_full_name %}({{ profile.user.get_full_name }}){% endif %}

{% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | {# Dirty hack. Will use django-guardian in the future. #} 11 | {% if user.username == profile.user.username %} 12 | 18 | {% endif %} 19 | 20 |
21 | {% trans 22 | {% if profile.user.get_full_name %} 23 |

{% trans "Name" %}
{{ profile.user.get_full_name }}

24 | {% endif %} 25 | {% if profile.age != 0 %} 26 |

{% trans "Age" %}
{{ profile.age }}

27 | {% endif %} 28 | {% if profile.website %} 29 |

{% trans "Website" %}
{{ profile.website }}

30 | {% endif %} 31 | {% if profile.location %} 32 |

{% trans "Location" %}
{{ profile.location }}

33 | {% endif %} 34 | {% if profile.about_me %} 35 |

{% trans "About me" %}
{{ profile.about_me }}

36 | {% endif %} 37 |
38 | 39 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /userena/management/commands/check_permissions.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from django.utils.encoding import smart_text 3 | 4 | from userena.compat import make_options 5 | from userena.models import UserenaSignup 6 | 7 | arguments = ( 8 | ('--no-output', { 9 | 'action': 'store_false', 10 | 'dest': 'output', 11 | 'default': True, 12 | 'help': 'Hide informational output.' 13 | }), 14 | ('--test', { 15 | 'action': 'store_true', 16 | 'dest': 'test', 17 | 'default': False, 18 | 'help': "Displays that it's testing management command. Don't use it yourself." 19 | }), 20 | ) 21 | 22 | 23 | class Command(BaseCommand): 24 | """ 25 | For unknown reason, users can get wrong permissions. 26 | This command checks that all permissions are correct. 27 | 28 | """ 29 | option_list = make_options(arguments) 30 | 31 | def add_arguments(self, parser): 32 | for arg, attrs in arguments: 33 | parser.add_argument(arg, **attrs) 34 | 35 | 36 | help = 'Check that user permissions are correct.' 37 | def handle(self, **options): 38 | permissions, users, warnings = UserenaSignup.objects.check_permissions() 39 | output = options.pop("output") 40 | test = options.pop("test") 41 | if test: 42 | self.stdout.write(40 * ".") 43 | self.stdout.write("\nChecking permission management command. Ignore output..\n\n") 44 | if output: 45 | for p in permissions: 46 | self.stdout.write("Added permission: %s\n" % p) 47 | 48 | for u in users: 49 | self.stdout.write("Changed permissions for user: %s\n" % smart_text(u, encoding='utf-8', strings_only=False)) 50 | 51 | for w in warnings: 52 | self.stdout.write("WARNING: %s\n" %w) 53 | 54 | if test: 55 | self.stdout.write("\nFinished testing permissions command.. continuing..\n") 56 | -------------------------------------------------------------------------------- /demo/demo/templates/500.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 4 | 5 | 6 | Userena crashed! 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
 
17 |
18 |
19 |

Userena crashed

20 | 21 |

Sorry for the inconvenience.

22 | 23 |

Please take the time to report an issue at Github so it doesn't happen again.

24 |
25 |
26 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | James Bennett: http://b-list.org (for creating django-registration) 2 | Grigoriy Petukhov: http://web-brains.com (for creating django-account) 3 | Lukasz Balcerzak: http://djangopeople.net/lukaszbalcerzak/ (for django-guardian and Polish translations) 4 | George Notaras: http://www.g-loaded.eu (for making userena 2.4 compatible) 5 | Jannis Leidel: http://enn.io (for fixing installation script) 6 | Guillaume Esquevin: http://www.platypus-creation.com (for improving translations) 7 | Aram Dulyan: http://interaction.net.au/ (for making userena compatible with Django 1.5 user model) 8 | Michał Jaworski: https://github.com/swistakm (for adding tox testing and updating Userena to work with Django 1.6.) 9 | Adi Jörg Sieker: http://sieker.io/ (for updating documentation and profile admin) 10 | Joel Bremson: https://github.com/jbremson (for updating documentation) 11 | Boris Savelev: https://github.com/bsavelev (for fixing some signal issues and allowing '@' in userena profile urls) 12 | nachouve: https://github.com/nachouve (for updating documentation) 13 | Ethno-urban: https://github.com/Ethno-urban (for updating documentation) 14 | Fabio Souto: http://fabiosouto.me/ (for updating documentation) 15 | ChangeSomeCode: https://github.com/ChangeSomeCode (for adding support for Django 1.7) 16 | Ghassen Telmoudi: https://github.com/pyghassen (for work on Python 3 support) 17 | Leonardo Orozco: https://github.com/leonardoo (for work on Python 3 support and multiple improvements) 18 | Guillaume Thomas: https://github.com/gtnx (for various fixes) 19 | Morten W. Hansen: https://github.com/mortenwh (for various fixes and translations) 20 | wordstospend: https://github.com/wordstospend (for updating documentation) 21 | Žan Anderle: https://github.com/zanderle (for improvements and compatibility fixes) 22 | Alex Willmer: https://github.com/moreati (for fixing django 1.4 builds) 23 | Andres Vargas: https://github.com/zodman (for fixing translations) 24 | Angel Ramboi: https://github.com/aramboi (for Romanian translations) 25 | smlz: https://github.com/smlz (for tremendous work in adding Django 1.7 and Django 1.8 support) 26 | -------------------------------------------------------------------------------- /userena/contrib/umessages/tests/test_managers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | from userena.contrib.umessages.models import (Message, MessageContact, 5 | MessageRecipient) 6 | 7 | User = get_user_model() 8 | 9 | 10 | class MessageManagerTests(TestCase): 11 | fixtures = ['users', 'messages'] 12 | 13 | def test_get_conversation(self): 14 | """ Test that the conversation is returned between two users """ 15 | user_1 = User.objects.get(pk=1) 16 | user_2 = User.objects.get(pk=2) 17 | 18 | messages = Message.objects.get_conversation_between(user_1, user_2) 19 | 20 | class MessageRecipientManagerTest(TestCase): 21 | fixtures = ['users', 'messages'] 22 | 23 | def test_count_unread_messages_for(self): 24 | """ Test the unread messages count for user """ 25 | jane = User.objects.get(pk=2) 26 | 27 | # Jane has one unread message from john 28 | unread_messages = MessageRecipient.objects.count_unread_messages_for(jane) 29 | 30 | self.assertEqual(unread_messages, 1) 31 | 32 | def test_count_unread_messages_between(self): 33 | """ Test the unread messages count between two users """ 34 | john = User.objects.get(pk=1) 35 | jane = User.objects.get(pk=2) 36 | 37 | # Jane should have one unread message from john 38 | unread_messages = MessageRecipient.objects.count_unread_messages_between(jane, john) 39 | 40 | self.assertEqual(unread_messages, 1) 41 | 42 | class MessageContactManagerTest(TestCase): 43 | fixtures = ['users', 'messages'] 44 | 45 | def test_get_contacts_for(self): 46 | """ Test if the correct contacts are returned """ 47 | john = User.objects.get(pk=1) 48 | contacts = MessageContact.objects.get_contacts_for(john) 49 | 50 | # There is only one contact for John, and that's Jane. 51 | self.assertEqual(len(contacts), 1) 52 | 53 | jane = User.objects.get(pk=2) 54 | self.assertEqual(contacts[0].um_to_user, 55 | jane) 56 | 57 | -------------------------------------------------------------------------------- /userena/contrib/umessages/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.test import TestCase 3 | 4 | from userena.contrib.umessages.models import Message, MessageRecipient, MessageContact 5 | from userena.utils import truncate_words 6 | 7 | User = get_user_model() 8 | 9 | 10 | class MessageContactTests(TestCase): 11 | fixtures = ['users', 'messages'] 12 | 13 | def test_string_formatting(self): 14 | """ Test the human representation of a message """ 15 | contact = MessageContact.objects.get(pk=1) 16 | correct_format = "john and jane" 17 | self.assertEqual(contact.__str__(), 18 | correct_format) 19 | 20 | def test_opposite_user(self): 21 | """ Test if the opposite user is returned """ 22 | contact = MessageContact.objects.get(pk=1) 23 | john = User.objects.get(pk=1) 24 | jane = User.objects.get(pk=2) 25 | 26 | # Test the opposites 27 | self.assertEqual(contact.opposite_user(john), 28 | jane) 29 | 30 | self.assertEqual(contact.opposite_user(jane), 31 | john) 32 | 33 | class MessageModelTests(TestCase): 34 | fixtures = ['users', 'messages'] 35 | 36 | def test_string_formatting(self): 37 | """ Test the human representation of a message """ 38 | message = Message.objects.get(pk=1) 39 | truncated_body = truncate_words(message.body, 10) 40 | self.assertEqual(message.__str__(), 41 | truncated_body) 42 | 43 | class MessageRecipientModelTest(TestCase): 44 | fixtures = ['users', 'messages'] 45 | 46 | def test_string_formatting(self): 47 | """ Test the human representation of a recipient """ 48 | recipient = MessageRecipient.objects.get(pk=1) 49 | 50 | valid_unicode = '%s' % (recipient.message) 51 | 52 | self.assertEqual(recipient.__str__(), 53 | valid_unicode) 54 | 55 | def test_new(self): 56 | """ Test if the message that is new is correct """ 57 | new_message = MessageRecipient.objects.get(pk=1) 58 | read_message = MessageRecipient.objects.get(pk=2) 59 | 60 | self.assertTrue(new_message.is_read()) 61 | self.assertFalse(read_message.is_read()) 62 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | 4 | userena = __import__('userena') 5 | 6 | readme_file = 'README.md' 7 | try: 8 | long_description = open(readme_file).read() 9 | except IOError: 10 | sys.stderr.write( 11 | "[ERROR] Cannot find file specified as " 12 | "``long_description`` (%s)\n" % readme_file 13 | ) 14 | sys.exit(1) 15 | 16 | install_requires = [ 17 | 'easy_thumbnails', 18 | 'django-guardian>=1.4.9', 19 | 'html2text==2014.12.29' 20 | ] 21 | 22 | try: 23 | from collections import OrderedDict 24 | except ImportError: 25 | install_requires.append('ordereddict') 26 | 27 | setup(name='django-userena', 28 | version=userena.get_version(), 29 | description='Complete user management application for Django', 30 | long_description=long_description, 31 | zip_safe=False, 32 | author='Petar Radosevic', 33 | author_email='petar@wunki.org', 34 | url='https://github.com/bread-and-pepper/django-userena/', 35 | download_url='https://github.com/bread-and-pepper/django-userena/downloads', 36 | packages=find_packages(exclude=['demo', 'demo.*']), 37 | include_package_data=True, 38 | install_requires = install_requires, 39 | test_suite='tests.main', 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'Environment :: Web Environment', 43 | 'Framework :: Django', 44 | 'Framework :: Django :: 1.5', 45 | 'Framework :: Django :: 1.6', 46 | 'Framework :: Django :: 1.7', 47 | 'Framework :: Django :: 1.8', 48 | 'Framework :: Django :: 1.9', 49 | 'Intended Audience :: Developers', 50 | 'License :: OSI Approved :: BSD License', 51 | 'Operating System :: OS Independent', 52 | 'Programming Language :: Python', 53 | 'Programming Language :: Python :: 2', 54 | 'Programming Language :: Python :: 2.6', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3', 57 | 'Programming Language :: Python :: 3.2', 58 | 'Programming Language :: Python :: 3.3', 59 | 'Programming Language :: Python :: 3.4', 60 | 'Programming Language :: Python :: 3.5', 61 | 'Topic :: Utilities' 62 | ], 63 | ) 64 | -------------------------------------------------------------------------------- /userena/contrib/umessages/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth import get_user_model 3 | from django.forms import widgets 4 | from django.utils.translation import ugettext_lazy as _ 5 | 6 | 7 | class CommaSeparatedUserInput(widgets.Input): 8 | input_type = 'text' 9 | 10 | def render(self, name, value, attrs=None): 11 | if value is None: 12 | value = '' 13 | elif isinstance(value, (list, tuple)): 14 | value = (', '.join([user.username for user in value])) 15 | return super(CommaSeparatedUserInput, self).render(name, value, attrs) 16 | 17 | class CommaSeparatedUserField(forms.Field): 18 | """ 19 | A :class:`CharField` that exists of comma separated usernames. 20 | 21 | :param recipient_filter: 22 | Optional function which receives as :class:`User` as parameter. The 23 | function should return ``True`` if the user is allowed or ``False`` if 24 | the user is not allowed. 25 | 26 | :return: 27 | A list of :class:`User`. 28 | 29 | """ 30 | widget = CommaSeparatedUserInput 31 | 32 | def __init__(self, *args, **kwargs): 33 | recipient_filter = kwargs.pop('recipient_filter', None) 34 | self._recipient_filter = recipient_filter 35 | super(CommaSeparatedUserField, self).__init__(*args, **kwargs) 36 | 37 | def clean(self, value): 38 | super(CommaSeparatedUserField, self).clean(value) 39 | 40 | names = set(value.split(',')) 41 | names_set = set([name.strip() for name in names]) 42 | users = list(get_user_model().objects.filter(username__in=names_set)) 43 | 44 | # Check for unknown names. 45 | unknown_names = names_set ^ set([user.username for user in users]) 46 | 47 | recipient_filter = self._recipient_filter 48 | invalid_users = [] 49 | if recipient_filter is not None: 50 | for r in users: 51 | if recipient_filter(r) is False: 52 | users.remove(r) 53 | invalid_users.append(r.username) 54 | 55 | if unknown_names or invalid_users: 56 | humanized_usernames = ', '.join(list(unknown_names) + invalid_users) 57 | raise forms.ValidationError(_("The following usernames are incorrect: %(users)s.") % {'users': humanized_usernames}) 58 | 59 | return users 60 | -------------------------------------------------------------------------------- /demo/createdb.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | import psycopg2 3 | import os 4 | from wsgi import * 5 | 6 | def create_dbs(): 7 | print("create_dbs: let's go.") 8 | django_settings = __import__(os.environ['DJANGO_SETTINGS_MODULE'], fromlist='DATABASES') 9 | print("create_dbs: got settings.") 10 | databases = django_settings.DATABASES 11 | for name, db in databases.iteritems(): 12 | host = db['HOST'] 13 | user = db['USER'] 14 | password = db['PASSWORD'] 15 | port = db['PORT'] 16 | db_name = db['NAME'] 17 | db_type = db['ENGINE'] 18 | # see if it is mysql 19 | if db_type.endswith('mysql'): 20 | print 'creating database %s on %s' % (db_name, host) 21 | db = MySQLdb.connect(user=user, 22 | passwd=password, 23 | host=host, 24 | port=port) 25 | cur = db.cursor() 26 | print("Check if database is already there.") 27 | cur.execute("""SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA 28 | WHERE SCHEMA_NAME = %s""", (db_name,)) 29 | results = cur.fetchone() 30 | if not results: 31 | print("Database %s doesn't exist, lets create it." % db_name) 32 | sql = """CREATE DATABASE IF NOT EXISTS %s """ % (db_name,) 33 | print("> %s" % sql) 34 | cur.execute(sql) 35 | print(".....") 36 | else: 37 | print("database already exists, moving on to next step.") 38 | # see if it is postgresql 39 | elif db_type.endswith('postgresql_psycopg2'): 40 | print 'creating database %s on %s' % (db_name, host) 41 | con = psycopg2.connect(host=host, user=user, password=password, port=port, database='postgres') 42 | con.set_isolation_level(0) 43 | cur = con.cursor() 44 | try: 45 | cur.execute('CREATE DATABASE %s' % db_name) 46 | except psycopg2.ProgrammingError as detail: 47 | print detail 48 | print 'moving right along...' 49 | else: 50 | print("ERROR: {0} is not supported by this script, you will need to create your database by hand.".format(db_type)) 51 | 52 | if __name__ == '__main__': 53 | import sys 54 | print("create_dbs start") 55 | create_dbs() 56 | print("create_dbs all done") 57 | -------------------------------------------------------------------------------- /userena/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from collections import defaultdict 3 | import django 4 | 5 | 6 | # this default dict will store all compat quirks for parameters of 7 | # django.contrib.auth views 8 | auth_views_compat_quirks = defaultdict(lambda: dict()) 9 | 10 | # below are quirks we must use because we can't change some userena API's 11 | # like (url names) 12 | if django.VERSION >= (1, 6, 0): # pragma: no cover 13 | # in django >= 1.6.0 django.contrib.auth.views.reset no longer looks 14 | # for django.contrib.auth.views.password_reset_done but for 15 | # password_reset_done named url. To avoid duplicating urls we 16 | # provide custom post_reset_redirect 17 | auth_views_compat_quirks['userena_password_reset'] = { 18 | 'post_reset_redirect': 'userena_password_reset_done', 19 | } 20 | 21 | # same case as above 22 | auth_views_compat_quirks['userena_password_reset_confirm'] = { 23 | 'post_reset_redirect': 'userena_password_reset_complete', 24 | } 25 | if django.VERSION >= (1, 7, 0): 26 | # Django 1.7 added a new argument to django.contrib.auth.views.password_reset 27 | # called html_email_template_name which allows us to pass it the html version 28 | # of the email 29 | auth_views_compat_quirks['html_email_template_name'] = 'userena/emails/password_reset_message.html' 30 | 31 | 32 | # below are backward compatibility fixes 33 | password_reset_uid_kwarg = 'uidb64' 34 | if django.VERSION < (1, 6, 0): # pragma: no cover 35 | # Django<1.6.0 uses uidb36, we construct urlpattern depending on this 36 | password_reset_uid_kwarg = 'uidb36' 37 | 38 | 39 | # SiteProfileNotAvailable compatibility 40 | if django.VERSION < (1, 7, 0): # pragma: no cover 41 | from django.contrib.auth.models import SiteProfileNotAvailable 42 | else: # pragma: no cover 43 | class SiteProfileNotAvailable(Exception): 44 | pass 45 | 46 | 47 | if django.VERSION < (1, 7, 0): 48 | from django.db.models import get_model 49 | else: 50 | from django.apps import apps 51 | get_model = apps.get_model 52 | 53 | 54 | # optparse/argparse compatibility helper for simple cases (long options only) 55 | # for an example useage see userena/management/commands/check_permissions.py 56 | if django.VERSION < (1, 8): 57 | from optparse import make_option 58 | def make_options(options): 59 | return list(make_option(opt, **attrs) for opt, attrs in options) 60 | else: 61 | def make_options(options): 62 | return () 63 | -------------------------------------------------------------------------------- /userena/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.6 on 2018-06-13 11:39 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import userena.managers 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='UserenaSignup', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('last_active', models.DateTimeField(blank=True, help_text='The last date that the user was active.', null=True, verbose_name='last active')), 23 | ('activation_key', models.CharField(blank=True, max_length=40, verbose_name='activation key')), 24 | ('invitation_key', models.CharField(blank=True, max_length=40, verbose_name='invitation key')), 25 | ('activation_notification_send', models.BooleanField(default=False, help_text='Designates whether this user has already got a notification about activating their account.', verbose_name='notification send')), 26 | ('email_unconfirmed', models.EmailField(blank=True, help_text='Temporary email address when the user requests an email change.', max_length=254, verbose_name='unconfirmed email address')), 27 | ('email_confirmation_key', models.CharField(blank=True, max_length=40, verbose_name='unconfirmed email verification key')), 28 | ('email_confirmation_key_created', models.DateTimeField(blank=True, null=True, verbose_name='creation date of email confirmation key')), 29 | ('invitation_status', models.CharField(choices=[('INV', 'Invitation Mail was sent'), ('PSWRST', 'Password was reset by user'), ('PRFEDIT', 'Profile was edited by user')], default='INV', max_length=7)), 30 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='userena_signup', to=settings.AUTH_USER_MODEL, verbose_name='user')), 31 | ], 32 | options={ 33 | 'verbose_name': 'userena registration', 34 | 'verbose_name_plural': 'userena registrations', 35 | }, 36 | managers=[ 37 | ('objects', userena.managers.UserenaManager()), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /userena/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [{"pk": 1, 2 | "model": "auth.user", 3 | "fields": {"username": "john", 4 | "first_name": "John", 5 | "last_name": "Doe", 6 | "is_active": true, 7 | "is_superuser": true, 8 | "is_staff": true, 9 | "last_login": "2010-08-18T03:07:03.019Z", 10 | "groups": [], 11 | "user_permissions": [], 12 | "password": "sha1$645fd$8d18b38b96f72363bc9438bc1d340081b87e0fbe", 13 | "email": "john@example.com", 14 | "date_joined": "2010-08-17T10:31:55.019Z"}}, 15 | {"pk": 2, 16 | "model": "auth.user", 17 | "fields": {"username": "jane", 18 | "first_name": "Jane", 19 | "last_name": "Doe", 20 | "is_active": true, 21 | "is_superuser": false, 22 | "is_staff": false, 23 | "last_login": "2010-08-18T03:15:19.019Z", 24 | "groups": [], 25 | "user_permissions": [], 26 | "password": "sha1$645fd$8d18b38b96f72363bc9438bc1d340081b87e0fbe", 27 | "email": "jane@example.com", 28 | "date_joined": "2010-08-18T03:13:57.019Z"}}, 29 | {"pk": 3, 30 | "model": "auth.user", 31 | "fields": {"username": "arie", 32 | "first_name": "Arie", 33 | "last_name": "de Beuker", 34 | "is_active": true, 35 | "is_superuser": false, 36 | "is_staff": false, 37 | "last_login": "2010-08-18T00:00:00.019Z", 38 | "groups": [], 39 | "user_permissions": [], 40 | "password": "sha1$645fd$8d18b38b96f72363bc9438bc1d340081b87e0fbe", 41 | "email": "arie@example.com", 42 | "date_joined": "2010-01-01T00:00:00.019Z"}}, 43 | 44 | {"pk": 1, 45 | "model": "userena.userenasignup", 46 | "fields": {"user": 1, 47 | "activation_key": "ALREADY_ACTIVATED", 48 | "last_active": "2010-08-17T13:37:03.019Z", 49 | "activation_notification_send": false}}, 50 | {"pk": 2, 51 | "model": "userena.userenasignup", 52 | "fields": {"user": 2, 53 | "activation_key": "ALREADY_ACTIVATED", 54 | "last_active": null, 55 | "activation_notification_send": false}}, 56 | {"pk": 3, 57 | "model": "userena.userenasignup", 58 | "fields": {"user": 3, 59 | "activation_key": "ALREADY_ACTIVATED", 60 | "last_active": null, 61 | "activation_notification_send": false}} 62 | ] 63 | -------------------------------------------------------------------------------- /userena/templates/userena/profile_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'userena/base_userena.html' %} 2 | {% load i18n %} 3 | 4 | {% block title %}{% blocktrans with profile.user.username as username %}{{ username }}'s profile.{% endblocktrans %}{% endblock %} 5 | {% block content_title %}

{{ profile.user.username }} {% if profile.user.get_full_name %}({{ profile.user.get_full_name }}){% endif %}

{% endblock %} 6 | 7 | {% block content %} 8 |
9 | {% block profile_navigation %} 10 | {# Dirty hack. Will use django-guardian in the future. #} 11 | {% if user.username == profile.user.username %} 12 | 20 | {% endif %} 21 | {% endblock %} 22 | 23 |
24 | {% block profile_details %} 25 | {% trans 26 |
27 | {% block profile_definition_list %} 28 | {% if profile.user.get_full_name %} 29 |
{% trans "Name" %}
30 |
{{ profile.user.get_full_name }}
31 | {% endif %} 32 | {% if profile.user.email and not hide_email %} 33 |
{% trans "Email" %}
34 |
{{ profile.user.email }}
35 | {% endif %} 36 | {% if profile.age %} 37 |
{% trans "Age" %}
38 |
{{ profile.age }}
39 | {% endif %} 40 | {% if profile.website %} 41 |
{% trans "Website" %}
42 |
{{ profile.website|urlize }}
43 | {% endif %} 44 | {% if profile.location %} 45 |
{% trans "Location" %}
46 |
{{ profile.location }}
47 | {% endif %} 48 | {% if profile.about_me %} 49 |
{% trans "About me" %}
50 |
{{ profile.about_me }}
51 | {% endif %} 52 | {% endblock %} 53 |
54 | {% endblock %} 55 |
56 |
57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /demo/locale/nl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2010-10-27 10:05-0500\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Language: \n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | #: settings.py:36 22 | msgid "English" 23 | msgstr "Engels" 24 | 25 | #: settings.py:37 26 | msgid "Dutch" 27 | msgstr "Nederlands" 28 | 29 | #: profiles/models.py:11 30 | msgid "Male" 31 | msgstr "" 32 | 33 | #: profiles/models.py:12 34 | msgid "Female" 35 | msgstr "" 36 | 37 | #: profiles/models.py:14 38 | msgid "gender" 39 | msgstr "" 40 | 41 | #: profiles/models.py:18 42 | msgid "website" 43 | msgstr "" 44 | 45 | #: profiles/models.py:19 46 | msgid "location" 47 | msgstr "" 48 | 49 | #: profiles/models.py:20 50 | msgid "birth date" 51 | msgstr "" 52 | 53 | #: profiles/models.py:21 54 | msgid "about me" 55 | msgstr "" 56 | 57 | #: templates/404.html:4 58 | msgid "Page not found" 59 | msgstr "" 60 | 61 | #: templates/404.html:7 62 | msgid "Page could not be found." 63 | msgstr "" 64 | 65 | #: templates/404.html:8 66 | msgid "" 67 | "If you think this is a bug in userena, please create an issue at Github." 68 | msgstr "" 69 | 70 | #: templates/base.html:32 71 | #, fuzzy 72 | msgid "Choose language" 73 | msgstr "Verander taal" 74 | 75 | #: templates/base.html:39 76 | msgid "Change language" 77 | msgstr "Verander taal" 78 | 79 | #: templates/base.html:51 80 | msgid "Home" 81 | msgstr "Thuis" 82 | 83 | #: templates/base.html:53 84 | msgid "Profiles" 85 | msgstr "" 86 | 87 | #: templates/base.html:54 88 | #, fuzzy 89 | msgid "Account settings" 90 | msgstr "Accounts" 91 | 92 | #: templates/base.html:55 93 | msgid "Signout" 94 | msgstr "Uitloggen" 95 | 96 | #: templates/base.html:57 97 | msgid "Signin" 98 | msgstr "Inloggen" 99 | 100 | #: templates/base.html:58 101 | msgid "Signup" 102 | msgstr "Inschrijven" 103 | 104 | #: templates/base.html:67 105 | msgid "Fork me hard" 106 | msgstr "Fork me!" 107 | 108 | #: templates/base.html:74 109 | msgid "Made by" 110 | msgstr "Gemaakt door" 111 | 112 | #~ msgid "Your account" 113 | #~ msgstr "Jouw account" 114 | -------------------------------------------------------------------------------- /userena/contrib/umessages/tests/test_fields.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | 6 | from django.test import TestCase 7 | from django import forms 8 | 9 | from userena.contrib.umessages.fields import CommaSeparatedUserField 10 | 11 | class CommaSeparatedTestForm(forms.Form): 12 | users = CommaSeparatedUserField() 13 | 14 | def __init__(self, *args, **kwargs): 15 | super(CommaSeparatedTestForm, self).__init__(*args, **kwargs) 16 | self.fields['users']._recipient_filter = self.filter_jane 17 | 18 | def filter_jane(self, user): 19 | if user.username == 'jane': 20 | return False 21 | return True 22 | 23 | class CommaSeperatedFieldTests(TestCase): 24 | fixtures = ['users',] 25 | 26 | def test_invalid_data(self): 27 | # Test invalid data supplied to the field. 28 | invalid_data_dicts = [ 29 | # Empty username 30 | {'data': {'users': ''}, 31 | 'error': ('users', ['This field is required.'])}, 32 | # No data 33 | {'data': {}, 34 | 'error': ('users', ['This field is required.'])}, 35 | # A list 36 | {'data': {'users': []}, 37 | 'error': ('users', ['This field is required.'])}, 38 | # Forbidden username 39 | {'data': {'users': 'jane'}, 40 | 'error': ('users', ['The following usernames are incorrect: jane.'])}, 41 | # Non-existant username 42 | {'data': {'users': 'foo'}, 43 | 'error': ('users', ['The following usernames are incorrect: foo.'])}, 44 | # Multiple invalid usernames 45 | {'data': {'users': 'foo, bar'}, 46 | 'error': ('users', ['The following usernames are incorrect: (foo|bar), (foo|bar).'])}, 47 | # Valid and invalid 48 | {'data': {'users': 'foo, john, bar'}, 49 | 'error': ('users', ['The following usernames are incorrect: (foo|bar), (foo|bar).'])}, 50 | # Extra whitespace 51 | {'data': {'users': 'foo, john '}, 52 | 'error': ('users', ['The following usernames are incorrect: foo.'])}, 53 | 54 | ] 55 | for invalid_dict in invalid_data_dicts: 56 | form = CommaSeparatedTestForm(data=invalid_dict['data']) 57 | self.assertFalse(form.is_valid()) 58 | # self.assertEqual(form.errors[invalid_dict['error'][0]], 59 | # invalid_dict['error'][1]) 60 | self.assertTrue(re.match( 61 | invalid_dict['error'][1][0], 62 | form.errors[invalid_dict['error'][0]][0] 63 | )) 64 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Userena documentation master file, created by 2 | sphinx-quickstart on Fri Jul 2 09:28:08 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Userena Introduction 7 | ==================== 8 | 9 | This documentation covers |release| release of django-userena application. 10 | A Django application that takes care of your account needs. 11 | 12 | Why userena? 13 | ================ 14 | 15 | Because we have done the hard work for you. Userena supplies you with signup, 16 | signin, account editing, privacy settings and private messaging. All you have 17 | to do is plug it into your project and you will have created account 18 | management with the following options: 19 | 20 | - User has to **activate** their account by clicking on a activation link 21 | in an email sent to them. 22 | 23 | - **Permissions** for viewing, changing and deleting accounts is 24 | implemented on an user and object basis with the help of django-guardian. 25 | 26 | - Optionally **secure** userena by using https. If you change the settings 27 | to use https, userena will switch to the secure protocol on it's views 28 | and emails. 29 | 30 | - All **templates** are already supplied for you. Only override those that 31 | don't fit with your needs. 32 | 33 | - Mugshots are supplied by **Gravatar** or uploaded by the user. The 34 | default mugshot can be set in the settings. 35 | 36 | - **Messaging** system between users that either get's displayed as 37 | conversations (iPhone like) or sorted per subject (Gmail). 38 | 39 | Help out 40 | ======== 41 | 42 | Found a bug in userena? File an issue at Github. Have an improvement? Fork it 43 | and add it, or if you can't code it, contact us to do it. 44 | 45 | 46 | Deprecation warnigns 47 | ==================== 48 | 49 | 2.0.0 version: 50 | 51 | - ``userena.utils.get_user_model()`` is deprecated and will be removed in 52 | version 3.0.0. Use ``django.contrib.auth.get_user_model()`` 53 | 54 | 55 | Changes and releases 56 | ==================== 57 | 58 | For changes history and available releases see following pages on GitHub 59 | repository: 60 | 61 | * `UDATES.md `_ 62 | * `releases `_ 63 | 64 | 65 | Contents 66 | ======== 67 | 68 | .. toctree:: 69 | :maxdepth: 2 70 | 71 | installation 72 | settings 73 | signals 74 | commands 75 | faq 76 | api/index 77 | 78 | Contrib: uMessages 79 | ~~~~~~~~~~~~~~~~~~ 80 | 81 | .. toctree:: 82 | :maxdepth: 3 83 | 84 | contrib/umessages/index 85 | 86 | Indices and tables 87 | ================== 88 | 89 | * :ref:`genindex` 90 | * :ref:`search` 91 | -------------------------------------------------------------------------------- /userena/tests/test_backends.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth import get_user_model 3 | 4 | from userena.backends import UserenaAuthenticationBackend 5 | 6 | User = get_user_model() 7 | 8 | 9 | class UserenaAuthenticationBackendTests(TestCase): 10 | """ 11 | Test the ``UserenaAuthenticationBackend`` which should return a ``User`` 12 | when supplied with a username/email and a correct password. 13 | 14 | """ 15 | fixtures = ['users',] 16 | backend = UserenaAuthenticationBackend() 17 | 18 | def test_with_username(self): 19 | """ Test the backend when usernames are supplied. """ 20 | # Invalid usernames or passwords 21 | invalid_data_dicts = [ 22 | # Invalid password 23 | {'identification': 'john', 24 | 'password': 'inhalefish'}, 25 | # Invalid username 26 | {'identification': 'alice', 27 | 'password': 'blowfish'}, 28 | ] 29 | for invalid_dict in invalid_data_dicts: 30 | result = self.backend.authenticate(identification=invalid_dict['identification'], 31 | password=invalid_dict['password']) 32 | self.assertFalse(isinstance(result, User)) 33 | 34 | # Valid username and password 35 | result = self.backend.authenticate(identification='john', 36 | password='blowfish') 37 | self.assertTrue(isinstance(result, User)) 38 | 39 | def test_with_email(self): 40 | """ Test the backend when email address is supplied """ 41 | # Invalid e-mail adressses or passwords 42 | invalid_data_dicts = [ 43 | # Invalid password 44 | {'identification': 'john@example.com', 45 | 'password': 'inhalefish'}, 46 | # Invalid e-mail address 47 | {'identification': 'alice@example.com', 48 | 'password': 'blowfish'}, 49 | ] 50 | for invalid_dict in invalid_data_dicts: 51 | result = self.backend.authenticate(identification=invalid_dict['identification'], 52 | password=invalid_dict['password']) 53 | self.assertFalse(isinstance(result, User)) 54 | 55 | # Valid e-email address and password 56 | result = self.backend.authenticate(identification='john@example.com', 57 | password='blowfish') 58 | self.assertTrue(isinstance(result, User)) 59 | 60 | def test_get_user(self): 61 | """ Test that the user is returned """ 62 | user = self.backend.get_user(1) 63 | self.assertEqual(user.username, 'john') 64 | 65 | # None should be returned when false id. 66 | user = self.backend.get_user(99) 67 | self.assertFalse(user) 68 | -------------------------------------------------------------------------------- /demo/demo/settings_dotcloud.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | import json 4 | 5 | DEBUG = False 6 | TEMPLATE_DEBUG = DEBUG 7 | with open('/home/dotcloud/environment.json') as f: 8 | env = json.load(f) 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 12 | 'NAME': 'django_userena', 13 | 'USER': env['DOTCLOUD_DB_SQL_LOGIN'], 14 | 'PASSWORD': env['DOTCLOUD_DB_SQL_PASSWORD'], 15 | 'HOST': env['DOTCLOUD_DB_SQL_HOST'], 16 | 'PORT': int(env['DOTCLOUD_DB_SQL_PORT']), 17 | } 18 | } 19 | 20 | # Email settings 21 | EMAIL_HOST = 'smtp.gmail.com' 22 | EMAIL_HOST_USER = env['GMAIL_USER'] 23 | EMAIL_HOST_PASSWORD = env['GMAIL_PASSWORD'] 24 | EMAIL_PORT = 587 25 | EMAIL_USE_TLS = True 26 | DEFAULT_FROM_EMAIL = "Userena " 27 | 28 | # Media and static 29 | MEDIA_ROOT = '/home/dotcloud/data/media/' 30 | STATIC_ROOT = '/home/dotcloud/volatile/static/' 31 | 32 | # Logging 33 | LOGGING = { 34 | 'version': 1, 35 | 'disable_existing_loggers': True, 36 | 'formatters': { 37 | 'verbose': { 38 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 39 | }, 40 | 'simple': { 41 | 'format': '%(levelname)s %(message)s' 42 | }, 43 | }, 44 | 'handlers': { 45 | 'null': { 46 | 'level':'DEBUG', 47 | 'class':'django.utils.log.NullHandler', 48 | }, 49 | 'console': { 50 | 'level': 'DEBUG', 51 | 'class': 'logging.StreamHandler', 52 | 'formatter': 'verbose' 53 | }, 54 | 'log_file': { 55 | 'level': 'DEBUG', 56 | 'class': 'logging.handlers.RotatingFileHandler', 57 | 'formatter': 'verbose', 58 | 'filename': '/var/log/supervisor/userena.log', 59 | 'maxBytes': 1024*1024*25, # 25 MB 60 | 'backupCount': 5, 61 | }, 62 | 'mail_admins': { 63 | 'level': 'ERROR', 64 | 'class': 'django.utils.log.AdminEmailHandler' 65 | } 66 | }, 67 | 'loggers': { 68 | 'django': { 69 | 'handlers': ['console', 'log_file', 'mail_admins'], 70 | 'level': 'INFO', 71 | 'propagate': True, 72 | }, 73 | 'django.request': { 74 | 'handlers': ['console', 'log_file', 'mail_admins'], 75 | 'level': 'ERROR', 76 | 'propagate': False, 77 | }, 78 | 'django.db.backends': { 79 | 'handlers': ['console', 'log_file', 'mail_admins'], 80 | 'level': 'INFO', 81 | 'propagate': False, 82 | }, 83 | # Catch All Logger -- Captures any other logging 84 | '': { 85 | 'handlers': ['console', 'log_file', 'mail_admins'], 86 | 'level': 'INFO', 87 | 'propagate': True, 88 | } 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /userena/contrib/umessages/templatetags/umessages_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from userena.contrib.umessages.models import MessageRecipient 4 | 5 | import re 6 | 7 | register = template.Library() 8 | 9 | class MessageCount(template.Node): 10 | def __init__(self, um_from_user, var_name, um_to_user=None): 11 | self.user = template.Variable(um_from_user) 12 | self.var_name = var_name 13 | if um_to_user: 14 | self.um_to_user = template.Variable(um_to_user) 15 | else: self.um_to_user = um_to_user 16 | 17 | def render(self, context): 18 | try: 19 | user = self.user.resolve(context) 20 | except template.VariableDoesNotExist: 21 | return '' 22 | 23 | if not self.um_to_user: 24 | message_count = MessageRecipient.objects.count_unread_messages_for(user) 25 | 26 | else: 27 | try: 28 | um_to_user = self.um_to_user.resolve(context) 29 | except template.VariableDoesNotExist: 30 | return '' 31 | 32 | message_count = MessageRecipient.objects.count_unread_messages_between(user, 33 | um_to_user) 34 | 35 | context[self.var_name] = message_count 36 | 37 | return '' 38 | 39 | @register.tag 40 | def get_unread_message_count_for(parser, token): 41 | """ 42 | Returns the unread message count for a user. 43 | 44 | Syntax:: 45 | 46 | {% get_unread_message_count_for [user] as [var_name] %} 47 | 48 | Example usage:: 49 | 50 | {% get_unread_message_count_for pero as message_count %} 51 | 52 | """ 53 | try: 54 | tag_name, arg = token.contents.split(None, 1) 55 | except ValueError: 56 | raise template.TemplateSyntaxError("%s tag requires arguments" % token.contents.split()[0]) 57 | m = re.search(r'(.*?) as (\w+)', arg) 58 | if not m: 59 | raise template.TemplateSyntaxError("%s tag had invalid arguments" % tag_name) 60 | user, var_name = m.groups() 61 | return MessageCount(user, var_name) 62 | 63 | @register.tag 64 | def get_unread_message_count_between(parser, token): 65 | """ 66 | Returns the unread message count between two users. 67 | 68 | Syntax:: 69 | 70 | {% get_unread_message_count_between [user] and [user] as [var_name] %} 71 | 72 | Example usage:: 73 | 74 | {% get_unread_message_count_between funky and wunki as message_count %} 75 | 76 | """ 77 | try: 78 | tag_name, arg = token.contents.split(None, 1) 79 | except ValueError: 80 | raise template.TemplateSyntaxError("%s tag requires arguments" % token.contents.split()[0]) 81 | m = re.search(r'(.*?) and (.*?) as (\w+)', arg) 82 | if not m: 83 | raise template.TemplateSyntaxError("%s tag had invalid arguments" % tag_name) 84 | um_from_user, um_to_user, var_name = m.groups() 85 | return MessageCount(um_from_user, var_name, um_to_user) 86 | -------------------------------------------------------------------------------- /userena/runtests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | 6 | # fix sys path so we don't need to setup PYTHONPATH 7 | sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) 8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'userena.runtests.settings' 9 | 10 | import django 11 | 12 | if django.VERSION >= (1, 7, 0): 13 | # starting from 1.7.0 we need to run setup() in order to populate 14 | # app config 15 | django.setup() 16 | 17 | from django.conf import settings 18 | from django.test.utils import get_runner 19 | 20 | 21 | def usage(): 22 | return """ 23 | Usage: python runtests.py [app] [-p ] 24 | 25 | """ 26 | 27 | 28 | def main(): 29 | TestRunner = get_runner(settings) 30 | 31 | # Ugly parameter parsing. We probably want to improve that in future 32 | # or just use default django test command. This may be problematic, 33 | # knowing how testing in Django changes from version to version. 34 | if '-p' in sys.argv: 35 | try: 36 | pos = sys.argv.index('-p') 37 | pattern = sys.argv.pop(pos) and sys.argv.pop(pos) 38 | except IndexError: 39 | print(usage()) 40 | sys.exit(1) 41 | else: 42 | pattern = None 43 | 44 | test_modules = sys.argv[1:] 45 | 46 | test_runner = TestRunner(verbosity=2, failfast=False, pattern=pattern) 47 | 48 | if len(sys.argv) > 1: 49 | test_modules = sys.argv[1:] 50 | elif len(sys.argv) == 1: 51 | test_modules = [] 52 | else: 53 | print(usage()) 54 | sys.exit(1) 55 | 56 | if (1, 6, 0) <= django.VERSION < (1, 9, 0): 57 | # this is a compat hack because in django>=1.6.0 you must provide 58 | # module like "userena.contrib.umessages" not "umessages" 59 | from django.db.models import get_app 60 | test_modules = [ 61 | # be more strict by adding .tests to not run umessages tests twice 62 | # if both userena and umessages are tested 63 | get_app(module_name).__name__[:-7] + ".tests" 64 | for module_name 65 | in test_modules 66 | ] 67 | elif django.VERSION >= (1, 9, 0): 68 | from django.apps import apps 69 | test_modules = [ 70 | # be more strict by adding .tests to not run umessages tests twice 71 | # if both userena and umessages are tested 72 | apps.get_app_config(module_name).name + ".tests" 73 | for module_name 74 | in test_modules 75 | ] 76 | 77 | if django.VERSION < (1, 7, 0): 78 | # starting from 1.7.0 built in django migrations are run 79 | # for older releases this patch is required to enable testing with 80 | # migrations 81 | from south.management.commands import patch_for_test_db_setup 82 | patch_for_test_db_setup() 83 | 84 | failures = test_runner.run_tests(test_modules or ['userena']) 85 | sys.exit(failures) 86 | 87 | 88 | if __name__ == '__main__': 89 | main() 90 | -------------------------------------------------------------------------------- /userena/mail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | from django.conf import settings 5 | from django.template.loader import render_to_string 6 | from django.utils.translation import ugettext as _ 7 | from django.core.mail import EmailMultiAlternatives 8 | 9 | from userena import settings as userena_settings 10 | 11 | from html2text import html2text 12 | 13 | def send_mail(subject, message_plain, message_html, email_from, email_to, 14 | custom_headers={}, attachments=()): 15 | """ 16 | Build the email as a multipart message containing 17 | a multipart alternative for text (plain, HTML) plus 18 | all the attached files. 19 | """ 20 | if not message_plain and not message_html: 21 | raise ValueError(_("Either message_plain or message_html should be not None")) 22 | 23 | if not message_plain: 24 | message_plain = html2text(message_html) 25 | 26 | message = {} 27 | 28 | message['subject'] = subject 29 | message['body'] = message_plain 30 | message['from_email'] = email_from 31 | message['to'] = email_to 32 | if attachments: 33 | message['attachments'] = attachments 34 | if custom_headers: 35 | message['headers'] = custom_headers 36 | 37 | msg = EmailMultiAlternatives(**message) 38 | if message_html: 39 | msg.attach_alternative(message_html, "text/html") 40 | msg.send() 41 | 42 | 43 | def wrap_attachment(): 44 | pass 45 | 46 | 47 | class UserenaConfirmationMail(object): 48 | 49 | _message_txt = 'userena/emails/{0}_email_message{1}.txt' 50 | _message_html = 'userena/emails/{0}_email_message{1}.html' 51 | _subject_txt = 'userena/emails/{0}_email_subject{1}.txt' 52 | 53 | def __init__(self, context): 54 | self.context = context 55 | 56 | def generate_mail(self, type_mail, version=""): 57 | self.type_mail = type_mail 58 | self.message_txt = self._message_txt.format(type_mail, version) 59 | self.message_html = self._message_html.format(type_mail, version) 60 | self.subject_txt = self._subject_txt.format(type_mail, version) 61 | self.subject = self._subject() 62 | self.message_html = self._message_in_html() 63 | self.message = self._message_in_txt() 64 | 65 | def send_mail(self, email): 66 | send_mail(self.subject, self.message, 67 | self.message_html, settings.DEFAULT_FROM_EMAIL, 68 | [email]) 69 | 70 | def _message_in_html(self): 71 | if userena_settings.USERENA_HTML_EMAIL: 72 | return render_to_string(self.message_html, self.context) 73 | return None 74 | 75 | def _message_in_txt(self): 76 | if (not userena_settings.USERENA_HTML_EMAIL 77 | or not self.message_html 78 | or userena_settings.USERENA_USE_PLAIN_TEMPLATE): 79 | return render_to_string(self.message_txt, self.context) 80 | return None 81 | 82 | def _subject(self): 83 | subject = render_to_string(self.subject_txt, self.context) 84 | subject = ''.join(subject.splitlines()) 85 | return subject 86 | -------------------------------------------------------------------------------- /userena/tests/tests_middleware.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.http import HttpRequest 3 | from django.test import TestCase 4 | 5 | from userena.tests.profiles.models import Profile 6 | from userena.middleware import UserenaLocaleMiddleware 7 | from userena import settings as userena_settings 8 | from userena.utils import get_user_profile, get_profile_model 9 | 10 | User = get_user_model() 11 | 12 | 13 | def has_profile(user): 14 | """Test utility function to check if user has profile""" 15 | profile_model = get_profile_model() 16 | try: 17 | profile = user.get_profile() 18 | except AttributeError: 19 | related_name = profile_model._meta.get_field('user')\ 20 | .related_query_name() 21 | profile = getattr(user, related_name, None) 22 | except profile_model.DoesNotExist: 23 | profile = None 24 | 25 | return bool(profile) 26 | 27 | 28 | 29 | class UserenaLocaleMiddlewareTests(TestCase): 30 | """ Test the ``UserenaLocaleMiddleware`` """ 31 | fixtures = ['users', 'profiles'] 32 | 33 | def _get_request_with_user(self, user): 34 | """ Fake a request with an user """ 35 | request = HttpRequest() 36 | request.META = { 37 | 'SERVER_NAME': 'testserver', 38 | 'SERVER_PORT': 80, 39 | } 40 | request.method = 'GET' 41 | request.session = {} 42 | 43 | # Add user 44 | request.user = user 45 | return request 46 | 47 | def test_preference_user(self): 48 | """ Test the language preference of two users """ 49 | users = ((1, 'nl'), 50 | (2, 'en')) 51 | 52 | for pk, lang in users: 53 | user = User.objects.get(pk=pk) 54 | profile = get_user_profile(user=user) 55 | 56 | req = self._get_request_with_user(user) 57 | 58 | # Check that the user has this preference 59 | self.assertEqual(profile.language, lang) 60 | 61 | # Request should have a ``LANGUAGE_CODE`` with dutch 62 | UserenaLocaleMiddleware().process_request(req) 63 | self.assertEqual(req.LANGUAGE_CODE, lang) 64 | 65 | def test_without_profile(self): 66 | """ Middleware should do nothing when a user has no profile """ 67 | # Delete the profile 68 | Profile.objects.get(pk=1).delete() 69 | user = User.objects.get(pk=1) 70 | 71 | # User shouldn't have a profile 72 | self.assertFalse(has_profile(user)) 73 | 74 | req = self._get_request_with_user(user) 75 | UserenaLocaleMiddleware().process_request(req) 76 | 77 | self.assertFalse(hasattr(req, 'LANGUAGE_CODE')) 78 | 79 | def test_without_language_field(self): 80 | """ Middleware should do nothing if the profile has no language field """ 81 | userena_settings.USERENA_LANGUAGE_FIELD = 'non_existant_language_field' 82 | user = User.objects.get(pk=1) 83 | 84 | req = self._get_request_with_user(user) 85 | 86 | # Middleware should do nothing 87 | UserenaLocaleMiddleware().process_request(req) 88 | self.assertFalse(hasattr(req, 'LANGUAGE_CODE')) 89 | -------------------------------------------------------------------------------- /userena/tests/test_privacy.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.test import TestCase 3 | 4 | from userena.tests.profiles.models import Profile 5 | 6 | class PrivacyTests(TestCase): 7 | """ 8 | Privacy testing of views concerning profiles. 9 | 10 | Test the privacy of the views that are available with three type of users: 11 | 12 | - Anonymous: An user that is not signed in. 13 | - Registered: An user that is registered and signed in. 14 | - Superuser: An user that is administrator at the site. 15 | 16 | """ 17 | fixtures = ['users', 'profiles'] 18 | 19 | reg_user = {'username': 'jane', 20 | 'password': 'blowfish'} 21 | 22 | super_user = {'username': 'john', 23 | 'password': 'blowfish'} 24 | 25 | detail_profile_url = reverse('userena_profile_detail', 26 | kwargs={'username': 'john'}) 27 | 28 | edit_profile_url = reverse('userena_profile_edit', 29 | kwargs={'username': 'john'}) 30 | 31 | def _test_status_codes(self, url, users_status): 32 | """ 33 | Test if the status codes are corresponding to what that user should 34 | see. 35 | 36 | """ 37 | for user, status in users_status: 38 | if user: 39 | self.client.login(**user) 40 | response = self.client.get(url, follow=True) 41 | self.assertEqual(response.status_code, status) 42 | 43 | def test_detail_open_profile_view(self): 44 | """ Viewing an open profile should be visible to everyone """ 45 | profile = Profile.objects.get(pk=1) 46 | profile.privacy = 'open' 47 | profile.save() 48 | 49 | users_status = ( 50 | (None, 200), 51 | (self.reg_user, 200), 52 | (self.super_user, 200) 53 | ) 54 | self._test_status_codes(self.detail_profile_url, users_status) 55 | 56 | def test_detail_registered_profile_view(self): 57 | """ Viewing a users who's privacy is registered """ 58 | profile = Profile.objects.get(pk=1) 59 | profile.privacy = 'registered' 60 | profile.save() 61 | 62 | users_status = ( 63 | (None, 403), 64 | (self.reg_user, 200), 65 | (self.super_user, 200) 66 | ) 67 | self._test_status_codes(self.detail_profile_url, users_status) 68 | 69 | def test_detail_closed_profile_view(self): 70 | """ Viewing a closed profile should only by visible to the admin """ 71 | profile = Profile.objects.get(pk=1) 72 | profile.privacy = 'closed' 73 | profile.save() 74 | 75 | users_status = ( 76 | (None, 403), 77 | (self.reg_user, 403), 78 | (self.super_user, 200) 79 | ) 80 | self._test_status_codes(self.detail_profile_url, users_status) 81 | 82 | def test_edit_profile_view(self): 83 | """ Editing a profile should only be available to the owner and the admin """ 84 | profile = Profile.objects.get(pk=1) 85 | 86 | users_status = ( 87 | (None, 403), 88 | (self.reg_user, 403), 89 | (self.super_user, 200) 90 | ) 91 | self._test_status_codes(self.edit_profile_url, users_status) 92 | -------------------------------------------------------------------------------- /demo/demo/templates/static/promo.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |

django-userena
Accounts made beautifully simple

5 | 6 |

Userena is a Django application that supplies your Django project with 7 | full account management. It's a fully customizable application that takes 8 | care of the signup, activation, messaging and more. It's BSD licensed, which 9 | means you can use it commercially for free!

10 | 11 |

Got any feedback? Email me at petar@breadandpepper.com. For bugs you can report an issue at Github.

12 | 13 |
14 | 15 |
    16 |
  • What is userena?

    17 | 18 |

    Userena is an django application that supplies your project with full 19 | account management. It handles the signup, activation, signin etc. for you. 20 | It also supplies your users with an optional profile model that integrates 21 | gravatar avatars. Signup at 22 | this website and try it out for yourself. 23 |

    24 | 25 |
  • 26 |
  • It's free!

    27 | 28 |

    Userena is licensed under the BSD license. This means it's free for 29 | commercial use. You don't have to worry about any licensing..

    30 | 31 |
  • 32 |

    Used in production

    33 | 34 |

    Bread & Pepper uses userena 35 | in their own projects. Because of this we have often revised the code and 36 | spared you repeating this work. This has made us confident enough to be able 37 | to say that the the software is ready for production. Because it's open 38 | source, we also get feedback from the community that we use to build a solid 39 | product.

    40 | 41 |
  • 42 |
  • 100% test coverage

    43 | 44 |

    Account management is an important aspect of any website, therefore we have 45 | chosen for 100% test coverage for every bit of code. This reduces bugs and 46 | makes sure that we don't introduce any new ones when updating. 47 | 48 |

  • 49 |
  • Messaging

    50 | 51 |

    Userena comes with an optional, chat-like, messaging system. This allows your 52 | users to communicate with each other through your site. It's use is intuitive 53 | and easy!

    54 | 55 |
  • 56 |
  • Privacy

    57 | 58 |

    Permission for viewing other peoples profiles are managed by 59 | django-guardian. 60 | This way we can let the user decide if they want to share their profile to 61 | everyone, registered users or nobody. 62 |

    63 |
  • 64 |
65 | 79 | {% endblock %} 80 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " pickle to make pickle files" 22 | @echo " json to make JSON files" 23 | @echo " htmlhelp to make HTML files and a HTML help project" 24 | @echo " qthelp to make HTML files and a qthelp project" 25 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 26 | @echo " changes to make an overview of all changed/added/deprecated items" 27 | @echo " linkcheck to check all external links for integrity" 28 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 29 | 30 | clean: 31 | -rm -rf $(BUILDDIR)/* 32 | 33 | html: 34 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 35 | @echo 36 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 37 | 38 | dirhtml: 39 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 40 | @echo 41 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 42 | 43 | pickle: 44 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 45 | @echo 46 | @echo "Build finished; now you can process the pickle files." 47 | 48 | json: 49 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 50 | @echo 51 | @echo "Build finished; now you can process the JSON files." 52 | 53 | htmlhelp: 54 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 55 | @echo 56 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 57 | ".hhp project file in $(BUILDDIR)/htmlhelp." 58 | 59 | qthelp: 60 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 61 | @echo 62 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 63 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 64 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Userena.qhcp" 65 | @echo "To view the help file:" 66 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Userena.qhc" 67 | 68 | latex: 69 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 70 | @echo 71 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 72 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 73 | "run these through (pdf)latex." 74 | 75 | changes: 76 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 77 | @echo 78 | @echo "The overview file is in $(BUILDDIR)/changes." 79 | 80 | linkcheck: 81 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 82 | @echo 83 | @echo "Link check complete; look for any errors in the above output " \ 84 | "or in $(BUILDDIR)/linkcheck/output.txt." 85 | 86 | doctest: 87 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 88 | @echo "Testing of doctests in the sources finished, look at the " \ 89 | "results in $(BUILDDIR)/doctest/output.txt." 90 | -------------------------------------------------------------------------------- /userena/contrib/umessages/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Message', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 19 | ('body', models.TextField(verbose_name='body')), 20 | ('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='sent at')), 21 | ('sender_deleted_at', models.DateTimeField(null=True, verbose_name='sender deleted at', blank=True)), 22 | ], 23 | options={ 24 | 'ordering': ['-sent_at'], 25 | 'verbose_name': 'message', 26 | 'verbose_name_plural': 'messages', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='MessageContact', 31 | fields=[ 32 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 33 | ('latest_message', models.ForeignKey(verbose_name='latest message', to='umessages.Message')), 34 | ('um_from_user', models.ForeignKey(related_name='um_from_users', verbose_name='from user', to=settings.AUTH_USER_MODEL)), 35 | ('um_to_user', models.ForeignKey(related_name='um_to_users', verbose_name='to user', to=settings.AUTH_USER_MODEL)), 36 | ], 37 | options={ 38 | 'ordering': ['latest_message'], 39 | 'verbose_name': 'contact', 40 | 'verbose_name_plural': 'contacts', 41 | }, 42 | ), 43 | migrations.CreateModel( 44 | name='MessageRecipient', 45 | fields=[ 46 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 47 | ('read_at', models.DateTimeField(null=True, verbose_name='read at', blank=True)), 48 | ('deleted_at', models.DateTimeField(null=True, verbose_name='recipient deleted at', blank=True)), 49 | ('message', models.ForeignKey(verbose_name='message', to='umessages.Message')), 50 | ('user', models.ForeignKey(verbose_name='recipient', to=settings.AUTH_USER_MODEL)), 51 | ], 52 | options={ 53 | 'verbose_name': 'recipient', 54 | 'verbose_name_plural': 'recipients', 55 | }, 56 | ), 57 | migrations.AddField( 58 | model_name='message', 59 | name='recipients', 60 | field=models.ManyToManyField(related_name='received_messages', verbose_name='recipients', through='umessages.MessageRecipient', to=settings.AUTH_USER_MODEL), 61 | ), 62 | migrations.AddField( 63 | model_name='message', 64 | name='sender', 65 | field=models.ForeignKey(related_name='sent_messages', verbose_name='sender', to=settings.AUTH_USER_MODEL), 66 | ), 67 | migrations.AlterUniqueTogether( 68 | name='messagecontact', 69 | unique_together=set([('um_from_user', 'um_to_user')]), 70 | ), 71 | ] 72 | -------------------------------------------------------------------------------- /userena/tests/tests_utils.py: -------------------------------------------------------------------------------- 1 | import sys, re, six 2 | 3 | from django.test import TestCase 4 | from django.conf import settings 5 | from django.contrib.auth import get_user_model 6 | from django.utils.six.moves.urllib_parse import urlparse, parse_qs 7 | 8 | from userena.utils import (get_gravatar, signin_redirect, get_profile_model, 9 | get_protocol, generate_sha1) 10 | from userena import settings as userena_settings 11 | from userena.compat import SiteProfileNotAvailable 12 | 13 | 14 | class UtilsTests(TestCase): 15 | """ Test the extra utils methods """ 16 | fixtures = ['users'] 17 | 18 | def test_generate_sha(self): 19 | s1 = six.u('\xc5se') 20 | s2 = six.u('\xd8ystein') 21 | s3 = six.u('\xc6gir') 22 | h1 = generate_sha1(s1) 23 | h2 = generate_sha1(s2) 24 | h3 = generate_sha1(s3) 25 | # Check valid SHA1 activation key 26 | self.assertTrue(re.match('^[a-f0-9]{40}$', h1[1])) 27 | self.assertTrue(re.match('^[a-f0-9]{40}$', h2[1])) 28 | self.assertTrue(re.match('^[a-f0-9]{40}$', h3[1])) 29 | 30 | def test_get_gravatar(self): 31 | template = 's=%(size)s&d=%(type)s' 32 | 33 | # Check the defaults. 34 | parsed = urlparse(get_gravatar('alice@example.com')) 35 | self.assertEqual( 36 | parse_qs(parsed.query), 37 | parse_qs(template % {'size': 80, 'type': 'identicon'}) 38 | ) 39 | 40 | # Check different size 41 | parsed = urlparse(get_gravatar('alice@example.com', size=200)) 42 | self.assertEqual( 43 | parse_qs(parsed.query), 44 | parse_qs(template % {'size': 200, 'type': 'identicon'}) 45 | ) 46 | 47 | # Check different default 48 | parsed = urlparse(get_gravatar('alice@example.com', default='404')) 49 | self.assertEqual( 50 | parse_qs(parsed.query), 51 | parse_qs(template % {'size': 80, 'type': '404'}) 52 | ) 53 | 54 | def test_signin_redirect(self): 55 | """ 56 | Test redirect function which should redirect the user after a 57 | succesfull signin. 58 | 59 | """ 60 | # Test with a requested redirect 61 | self.assertEqual(signin_redirect(redirect='/accounts/'), '/accounts/') 62 | 63 | # Test with only the user specified 64 | user = get_user_model().objects.get(pk=1) 65 | self.assertEqual(signin_redirect(user=user), 66 | '/accounts/%s/' % user.username) 67 | 68 | # The ultimate fallback, probably never used 69 | self.assertEqual(signin_redirect(), settings.LOGIN_REDIRECT_URL) 70 | 71 | def test_get_profile_model(self): 72 | """ 73 | Test if the correct profile model is returned when 74 | ``get_profile_model()`` is called. 75 | 76 | """ 77 | # A non existent model should also raise ``SiteProfileNotAvailable`` 78 | # error. 79 | with self.settings(AUTH_PROFILE_MODULE='userena.FakeProfile'): 80 | self.assertRaises(SiteProfileNotAvailable, get_profile_model) 81 | 82 | # An error should be raised when there is no ``AUTH_PROFILE_MODULE`` 83 | # supplied. 84 | with self.settings(AUTH_PROFILE_MODULE=None): 85 | self.assertRaises(SiteProfileNotAvailable, get_profile_model) 86 | 87 | def test_get_protocol(self): 88 | """ Test if the correct protocol is returned """ 89 | self.assertEqual(get_protocol(), 'http') 90 | 91 | with self.settings(USERENA_USE_HTTPS=True): 92 | self.assertEqual(get_protocol(), 'https') 93 | -------------------------------------------------------------------------------- /demo/demo/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load i18n static %} 2 | 4 | 5 | 6 | {% block title %}Django Userena - Accounts for your Django application{% endblock %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | {% if messages %} 21 |
    22 | {% for message in messages %} 23 | {{ message }} 24 | {% endfor %} 25 |
26 | {% endif %} 27 | 28 |
29 |
30 | {% csrf_token %} 31 | 32 | 33 | 38 | 41 |
42 |
43 |
44 |
45 | 68 | {% trans 'Fork me hard' %} 69 | {% block content_title %}{% endblock %} 70 |
71 | {% block content %}{% endblock %} 72 |
73 | 74 |
75 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /userena/contrib/umessages/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q 3 | 4 | from userena.contrib.umessages import signals 5 | 6 | import datetime 7 | 8 | class MessageContactManager(models.Manager): 9 | """ Manager for the :class:`MessageContact` model """ 10 | 11 | def get_or_create(self, um_from_user, um_to_user, message): 12 | """ 13 | Get or create a Contact 14 | 15 | We override Django's :func:`get_or_create` because we want contact to 16 | be unique in a bi-directional manner. 17 | 18 | """ 19 | created = False 20 | try: 21 | contact = self.get(Q(um_from_user=um_from_user, um_to_user=um_to_user) | 22 | Q(um_from_user=um_to_user, um_to_user=um_from_user)) 23 | 24 | except self.model.DoesNotExist: 25 | created = True 26 | contact = self.create(um_from_user=um_from_user, 27 | um_to_user=um_to_user, 28 | latest_message=message) 29 | 30 | return (contact, created) 31 | 32 | def update_contact(self, um_from_user, um_to_user, message): 33 | """ Get or update a contacts information """ 34 | contact, created = self.get_or_create(um_from_user, 35 | um_to_user, 36 | message) 37 | 38 | # If the contact already existed, update the message 39 | if not created: 40 | contact.latest_message = message 41 | contact.save() 42 | return contact 43 | 44 | def get_contacts_for(self, user): 45 | """ 46 | Returns the contacts for this user. 47 | 48 | Contacts are other users that this user has received messages 49 | from or send messages to. 50 | 51 | :param user: 52 | The :class:`User` which to get the contacts for. 53 | 54 | """ 55 | contacts = self.filter(Q(um_from_user=user) | Q(um_to_user=user)) 56 | return contacts 57 | 58 | class MessageManager(models.Manager): 59 | """ Manager for the :class:`Message` model. """ 60 | 61 | def send_message(self, sender, um_to_user_list, body): 62 | """ 63 | Send a message from a user, to a user. 64 | 65 | :param sender: 66 | The :class:`User` which sends the message. 67 | 68 | :param um_to_user_list: 69 | A list which elements are :class:`User` to whom the message is for. 70 | 71 | :param message: 72 | String containing the message. 73 | 74 | """ 75 | msg = self.model(sender=sender, 76 | body=body) 77 | msg.save() 78 | 79 | # Save the recipients 80 | msg.save_recipients(um_to_user_list) 81 | msg.update_contacts(um_to_user_list) 82 | signals.email_sent.send(sender=None,msg=msg) 83 | 84 | return msg 85 | 86 | def get_conversation_between(self, um_from_user, um_to_user): 87 | """ Returns a conversation between two users """ 88 | messages = self.filter(Q(sender=um_from_user, recipients=um_to_user, 89 | sender_deleted_at__isnull=True) | 90 | Q(sender=um_to_user, recipients=um_from_user, 91 | messagerecipient__deleted_at__isnull=True)) 92 | return messages 93 | 94 | class MessageRecipientManager(models.Manager): 95 | """ Manager for the :class:`MessageRecipient` model. """ 96 | 97 | def count_unread_messages_for(self, user): 98 | """ 99 | Returns the amount of unread messages for this user 100 | 101 | :param user: 102 | A Django :class:`User` 103 | 104 | :return: 105 | An integer with the amount of unread messages. 106 | 107 | """ 108 | unread_total = self.filter(user=user, 109 | read_at__isnull=True, 110 | deleted_at__isnull=True).count() 111 | 112 | return unread_total 113 | 114 | def count_unread_messages_between(self, um_to_user, um_from_user): 115 | """ 116 | Returns the amount of unread messages between two users 117 | 118 | :param um_to_user: 119 | A Django :class:`User` for who the messages are for. 120 | 121 | :param um_from_user: 122 | A Django :class:`User` from whom the messages originate from. 123 | 124 | :return: 125 | An integer with the amount of unread messages. 126 | 127 | """ 128 | unread_total = self.filter(message__sender=um_from_user, 129 | user=um_to_user, 130 | read_at__isnull=True, 131 | deleted_at__isnull=True).count() 132 | 133 | return unread_total 134 | -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for Userena demo project. 2 | DEBUG = True 3 | TEMPLATE_DEBUG = DEBUG 4 | 5 | import os 6 | settings_dir = os.path.dirname(__file__) 7 | PROJECT_ROOT = os.path.abspath(os.path.dirname(settings_dir)) 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@example.com'), 11 | ) 12 | 13 | MANAGERS = ADMINS 14 | 15 | DATABASES = { 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': os.path.join(PROJECT_ROOT, 'private/development.db'), 19 | } 20 | } 21 | 22 | # Internationalization 23 | TIME_ZONE = 'America/Chicago' 24 | LANGUAGE_CODE = 'en-us' 25 | ugettext = lambda s: s 26 | LANGUAGES = ( 27 | ('en', ugettext('English')), 28 | ('nl', ugettext('Dutch')), 29 | ('fr', ugettext('French')), 30 | ('pl', ugettext('Polish')), 31 | ('pt', ugettext('Portugese')), 32 | ('pt-br', ugettext('Brazilian Portuguese')), 33 | ('es', ugettext('Spanish')), 34 | ('el', ugettext('Greek')), 35 | ) 36 | LOCALE_PATHS = ( 37 | os.path.join(PROJECT_ROOT, 'locale'), 38 | ) 39 | 40 | SITE_ID = 1 41 | 42 | # If you set this to False, Django will make some optimizations so as not 43 | # to load the internationalization machinery. 44 | USE_I18N = True 45 | 46 | # If you set this to False, Django will not format dates, numbers and 47 | # calendars according to the current locale. 48 | USE_L10N = True 49 | 50 | # If you set this to False, Django will not use timezone-aware datetimes. 51 | USE_TZ = True 52 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, 'public/media/') 53 | MEDIA_URL = '/media/' 54 | STATIC_ROOT = os.path.join(PROJECT_ROOT, 'public/static/') 55 | STATIC_URL = '/static/' 56 | 57 | STATICFILES_DIRS = ( 58 | os.path.join(PROJECT_ROOT, 'demo/static/'), 59 | ) 60 | 61 | # List of finder classes that know how to find static files in 62 | # various locations. 63 | STATICFILES_FINDERS = ( 64 | 'django.contrib.staticfiles.finders.FileSystemFinder', 65 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 66 | ) 67 | 68 | # Make this unique, and don't share it with anybody. 69 | SECRET_KEY = '_g-js)o8z#8=9pr1&05h^1_#)91sbo-)g^(*=-+epxmt4kc9m#' 70 | 71 | # List of callables that know how to import templates from various sources. 72 | TEMPLATE_LOADERS = ( 73 | 'django.template.loaders.filesystem.Loader', 74 | 'django.template.loaders.app_directories.Loader', 75 | # 'django.template.loaders.eggs.Loader', 76 | ) 77 | 78 | MIDDLEWARE_CLASSES = ( 79 | 'django.middleware.common.CommonMiddleware', 80 | 'django.contrib.sessions.middleware.SessionMiddleware', 81 | 'django.middleware.csrf.CsrfViewMiddleware', 82 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 83 | 'django.contrib.messages.middleware.MessageMiddleware', 84 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 85 | 'django.middleware.locale.LocaleMiddleware', 86 | 'userena.middleware.UserenaLocaleMiddleware', 87 | ) 88 | 89 | # Add the Guardian and userena authentication backends 90 | AUTHENTICATION_BACKENDS = ( 91 | 'userena.backends.UserenaAuthenticationBackend', 92 | 'guardian.backends.ObjectPermissionBackend', 93 | 'django.contrib.auth.backends.ModelBackend', 94 | ) 95 | 96 | # Settings used by Userena 97 | LOGIN_REDIRECT_URL = '/accounts/%(username)s/' 98 | LOGIN_URL = '/accounts/signin/' 99 | LOGOUT_URL = '/accounts/signout/' 100 | AUTH_PROFILE_MODULE = 'profiles.Profile' 101 | USERENA_DISABLE_PROFILE_LIST = True 102 | USERENA_MUGSHOT_SIZE = 140 103 | 104 | ROOT_URLCONF = 'demo.urls' 105 | WSGI_APPLICATION = 'demo.wsgi.application' 106 | 107 | TEMPLATE_DIRS = ( 108 | os.path.join(PROJECT_ROOT, 'demo/templates/'), 109 | ) 110 | 111 | INSTALLED_APPS = ( 112 | 'django.contrib.auth', 113 | 'django.contrib.contenttypes', 114 | 'django.contrib.sessions', 115 | 'django.contrib.sites', 116 | 'django.contrib.messages', 117 | 'django.contrib.staticfiles', 118 | 'django.contrib.admin', 119 | 'django.contrib.admindocs', 120 | 'guardian', 121 | 'south', 122 | 'userena', 123 | 'userena.contrib.umessages', 124 | 'profiles', 125 | ) 126 | 127 | LOGGING = { 128 | 'version': 1, 129 | 'disable_existing_loggers': False, 130 | 'filters': { 131 | 'require_debug_false': { 132 | '()': 'django.utils.log.RequireDebugFalse' 133 | } 134 | }, 135 | 'handlers': { 136 | 'mail_admins': { 137 | 'level': 'ERROR', 138 | 'filters': ['require_debug_false'], 139 | 'class': 'django.utils.log.AdminEmailHandler' 140 | } 141 | }, 142 | 'loggers': { 143 | 'django.request': { 144 | 'handlers': ['mail_admins'], 145 | 'level': 'ERROR', 146 | 'propagate': True, 147 | }, 148 | } 149 | } 150 | 151 | # Needed for Django guardian 152 | ANONYMOUS_USER_ID = -1 153 | 154 | # Test runner 155 | TEST_RUNNER = 'django_coverage.coverage_runner.CoverageRunner' 156 | -------------------------------------------------------------------------------- /userena/settings.py: -------------------------------------------------------------------------------- 1 | # Userena settings file. 2 | # 3 | # Please consult the docs for more information about each setting. 4 | 5 | from django.conf import settings 6 | gettext = lambda s: s 7 | 8 | 9 | USERENA_SIGNIN_AFTER_SIGNUP = getattr(settings, 10 | 'USERENA_SIGNIN_AFTER_SIGNUP', 11 | False) 12 | 13 | USERENA_REDIRECT_ON_SIGNOUT = getattr(settings, 14 | 'USERENA_REDIRECT_ON_SIGNOUT', 15 | None) 16 | 17 | USERENA_SIGNIN_REDIRECT_URL = getattr(settings, 18 | 'USERENA_SIGNIN_REDIRECT_URL', 19 | '/accounts/%(username)s/') 20 | 21 | USERENA_ACTIVATION_REQUIRED = getattr(settings, 22 | 'USERENA_ACTIVATION_REQUIRED', 23 | True) 24 | 25 | USERENA_ACTIVATION_DAYS = getattr(settings, 26 | 'USERENA_ACTIVATION_DAYS', 27 | 7) 28 | 29 | USERENA_ACTIVATION_NOTIFY = getattr(settings, 30 | 'USERENA_ACTIVATION_NOTIFY', 31 | True) 32 | 33 | USERENA_ACTIVATION_NOTIFY_DAYS = getattr(settings, 34 | 'USERENA_ACTIVATION_NOTIFY_DAYS', 35 | 5) 36 | 37 | USERENA_ACTIVATION_RETRY = getattr(settings, 38 | 'USERENA_ACTIVATION_RETRY', 39 | False) 40 | 41 | USERENA_ACTIVATED = getattr(settings, 42 | 'USERENA_ACTIVATED', 43 | 'ALREADY_ACTIVATED') 44 | 45 | USERENA_REMEMBER_ME_DAYS = getattr(settings, 46 | 'USERENA_REMEMBER_ME_DAYS', 47 | (gettext('a month'), 30)) 48 | 49 | USERENA_FORBIDDEN_USERNAMES = getattr(settings, 50 | 'USERENA_FORBIDDEN_USERNAMES', 51 | ('signup', 'signout', 'signin', 52 | 'activate', 'me', 'password')) 53 | DEFAULT_USERENA_USE_HTTPS = False 54 | 55 | # NOTE: It is only for internal use. All those settings should be refactored to only defaults 56 | # as specified in #452 57 | _USERENA_USE_HTTPS = getattr(settings, 'USERENA_USE_HTTPS', DEFAULT_USERENA_USE_HTTPS) 58 | 59 | 60 | USERENA_MUGSHOT_GRAVATAR = getattr(settings, 61 | 'USERENA_MUGSHOT_GRAVATAR', 62 | True) 63 | 64 | USERENA_MUGSHOT_GRAVATAR_SECURE = getattr(settings, 65 | 'USERENA_MUGSHOT_GRAVATAR_SECURE', 66 | _USERENA_USE_HTTPS) 67 | 68 | USERENA_MUGSHOT_DEFAULT = getattr(settings, 69 | 'USERENA_MUGSHOT_DEFAULT', 70 | 'identicon') 71 | 72 | USERENA_MUGSHOT_SIZE = getattr(settings, 73 | 'USERENA_MUGSHOT_SIZE', 74 | 80) 75 | 76 | USERENA_MUGSHOT_CROP_TYPE = getattr(settings, 77 | 'USERENA_MUGSHOT_CROP_TYPE', 78 | 'smart') 79 | 80 | USERENA_MUGSHOT_PATH = getattr(settings, 81 | 'USERENA_MUGSHOT_PATH', 82 | 'mugshots/') 83 | 84 | USERENA_DEFAULT_PRIVACY = getattr(settings, 85 | 'USERENA_DEFAULT_PRIVACY', 86 | 'registered') 87 | 88 | USERENA_DISABLE_PROFILE_LIST = getattr(settings, 89 | 'USERENA_DISABLE_PROFILE_LIST', 90 | False) 91 | 92 | USERENA_DISABLE_SIGNUP = getattr(settings, 93 | 'USERENA_DISABLE_SIGNUP', 94 | False) 95 | 96 | USERENA_USE_MESSAGES = getattr(settings, 97 | 'USERENA_USE_MESSAGES', 98 | True) 99 | 100 | USERENA_LANGUAGE_FIELD = getattr(settings, 101 | 'USERENA_LANGUAGE_FIELD', 102 | 'language') 103 | 104 | USERENA_WITHOUT_USERNAMES = getattr(settings, 105 | 'USERENA_WITHOUT_USERNAMES', 106 | False) 107 | 108 | USERENA_PROFILE_DETAIL_TEMPLATE = getattr( 109 | settings, 'USERENA_PROFILE_DETAIL_TEMPLATE', 'userena/profile_detail.html') 110 | 111 | USERENA_PROFILE_LIST_TEMPLATE = getattr( 112 | settings, 'USERENA_PROFILE_LIST_TEMPLATE', 'userena/profile_list.html') 113 | 114 | USERENA_HIDE_EMAIL = getattr(settings, 'USERENA_HIDE_EMAIL', False) 115 | 116 | USERENA_HTML_EMAIL = getattr(settings, 'USERENA_HTML_EMAIL', False) 117 | 118 | USERENA_USE_PLAIN_TEMPLATE = getattr(settings, 'USERENA_USE_PLAIN_TEMPLATE', not USERENA_HTML_EMAIL) 119 | 120 | USERENA_REGISTER_PROFILE = getattr(settings, 'USERENA_REGISTER_PROFILE', True) 121 | 122 | USERENA_REGISTER_USER = getattr(settings, 'USERENA_REGISTER_USER', True) 123 | -------------------------------------------------------------------------------- /UPDATES.md: -------------------------------------------------------------------------------- 1 | # UPDATES 2 | 3 | This file contains all the backwards-incompatible (since 1.0.1) and other 4 | significant (since 1.4.1) changes. 5 | 6 | 7 | ## Version 2.0.1 8 | 9 | Fixes and improvements: 10 | 11 | - Fixed missing `README.md` that caused installation failure (#517). 12 | - Use universal wheels when distributing the package. 13 | 14 | ## Version 2.0.0 15 | 16 | Backwards incompatible changes: 17 | 18 | - Drop support for Django 1.4 19 | - Add support for Django 1.9 20 | - Added new Django 1.9 migration for `userena` and `userena.contrib.umessages`. 21 | Old South migrations are still available in `userena.south_migrations` and 22 | `userena.contrib.umessages.south_migrations` but are deprecated and may be 23 | removed in future major releases. `South>1.0` is required for older versions 24 | of Django. 25 | - `django-guardian` requirement bumped to `<=1.4.1` as newer version (1.4.2) 26 | fails on tests. 27 | - removed all `{% load url from future %}` from userena templates for 28 | compatibility with Django 1.9 29 | - `sha_constructor()`, `smart_text()`, and `md5_constructor` removed from 30 | `userena.compat` 31 | - Use simple list literal as url patterns instead of 32 | `django.conf.urls.patterns()` function 33 | 34 | Deprecated features: 35 | 36 | - `userena.utils.get_user_model` is deprecated and will be removed in version 37 | 3.0.0. Use `django.contrib.auth.get_user_model` 38 | 39 | Fixes and improvements: 40 | 41 | - More nodes added to the travis test matrix. 42 | - Some documentation moved to `.md` files. 43 | - Deprecated unittest aliases replaced in tests. 44 | - Updated list of trove classifiers. 45 | - Added option to execute `runtests` script with pattern parameter to limit 46 | number of tests. 47 | - Added `{posargs}` to test command execution in `tox.ini` to improve developer 48 | experience during tests. 49 | - Minor documentation improvements. 50 | - Set `django-guardian<1.4.0` version for Django 1.5 and 1.6 tests. 51 | 52 | 53 | ## Version 1.5.1 54 | 55 | - Update url patterns to handle usernames starting with 'signup', 'signin', 56 | 'signout' strings (#502) 57 | 58 | ## Version 1.5.0 59 | 60 | Fixes and improvements: 61 | 62 | - Updated Norwegian translations and fixed issue with unicode characters sent 63 | to `utils.generate_sha1` (#472) 64 | - Fix `upload_to_mugshot` if model uses primary key diffrent than `id` (#475) 65 | - Minor compatibility improvements (#485) 66 | - Refactored mailer (#486) 67 | - Password reset email now inlcudes email template for django>=1.7 (#493) 68 | - Fixes to translations (#499) 69 | - Added Romanian translations (#500) 70 | 71 | Backwards incompatible changes: 72 | 73 | - django-guardian has version fixed to `<=1.3.1` due to django 1.4 compatibility 74 | 75 | 76 | ## Version 1.4.1 77 | 78 | Fixes and improvements: 79 | 80 | - Set `html2text==2014.12.29` requirement in order to remove nasty hack 81 | in `userena/mail.py` (#462). 82 | - Updated version in Sphinx docs (#461). 83 | - Added support for Python3.2 (#453). 84 | - Fixed issue causing malformed utf-8 usernames on user profile creation (#456). 85 | - Fixed issue with user creation on Python3.4 (#455). 86 | - Fixed randomly failing tests on travis (#446). 87 | 88 | 89 | ## Version 1.4.0 90 | 91 | Fixes and improvements: 92 | 93 | - Python3.3 and Python3.4 support added. 94 | 95 | 96 | ## Version 1.3.2 97 | 98 | Backwards incompatible changes: 99 | 100 | - Creating new user always creates new empty userena profile (fixes bug from 1.3.1). 101 | 102 | 103 | ## Version 1.3.1 104 | 105 | Backwards incompatible changes: 106 | 107 | - When USERENA_ACTIVATION_REQUIRED = False, creating new user does not automatically 108 | create userena profile (bug). 109 | 110 | 111 | ## Version 1.2.2 112 | 113 | Backwards incompatible changes: 114 | 115 | - Changed backwards relationships names for Umessages contrib application from 116 | `to_user` to `um_to_user` and `from_user` to `um_from_user`. 117 | 118 | 119 | ## Version 1.2.0 120 | 121 | Backwards incompatible changes: 122 | 123 | - This version updates Userena to be able to use the new `User` model found in 124 | Django 1.5. This does require the django-guardion > 1.5-dev. To make this 125 | work, there is a `get_user_model` function in `userena/utils.py` which is used 126 | to get the correct `User` model. 127 | 128 | 129 | ## Version 1.1.2 130 | 131 | Backwards incompatible changes: 132 | 133 | - Activation view no longer contains username. If you override 134 | `userena/templates/userena/emails/activation_email_message.txt` and 135 | `userena/templates/userena/emails/confirmation_email_message_new.txt` be sure 136 | to remove username from the URL. 137 | 138 | 139 | ## Version 1.1 140 | 141 | Backwards incompatible changes: 142 | 143 | - Userena now requires Django >= 1.3 because of the use of class based views. 144 | 145 | 146 | ## Version 1.0.1 147 | 148 | Backwards incompatible changes: 149 | 150 | - Removed the ``user`` relationship outside ``UserenaBaseProfile`` model. This 151 | allows the user to define it's own relationship and fixes a conflict while 152 | running ``manage.py test``. To migrate, place the ``user`` field inside your 153 | profile model, instead of inheriting it from the ``abstract`` class. 154 | -------------------------------------------------------------------------------- /userena/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | from django.core.management import call_command 6 | from django.contrib.auth import get_user_model 7 | from django.contrib.auth.models import Permission 8 | from django.contrib.contenttypes.models import ContentType 9 | 10 | from userena.models import UserenaSignup 11 | from userena.managers import ASSIGNED_PERMISSIONS 12 | from userena import settings as userena_settings 13 | from userena.utils import get_profile_model 14 | 15 | from guardian.models import UserObjectPermission 16 | 17 | import datetime 18 | 19 | User = get_user_model() 20 | 21 | class CleanExpiredTests(TestCase): 22 | user_info = {'username': 'alice', 23 | 'password': 'swordfish', 24 | 'email': 'alice@example.com'} 25 | 26 | def test_clean_expired(self): 27 | """ 28 | Test if ``clean_expired`` deletes all users which ``activation_key`` 29 | is expired. 30 | 31 | """ 32 | # Create an account which is expired. 33 | user = UserenaSignup.objects.create_user(**self.user_info) 34 | user.date_joined -= datetime.timedelta(days=userena_settings.USERENA_ACTIVATION_DAYS + 1) 35 | user.save() 36 | 37 | # There should be one account now 38 | User.objects.get(username=self.user_info['username']) 39 | 40 | # Clean it. 41 | call_command('clean_expired') 42 | 43 | self.assertEqual(User.objects.filter(username=self.user_info['username']).count(), 0) 44 | 45 | class CheckPermissionTests(TestCase): 46 | user_info = {'username': 'alice', 47 | 'password': 'swordfish', 48 | 'email': 'alice@example.com'} 49 | 50 | def test_check_permissions(self): 51 | # Create a new account. 52 | user = UserenaSignup.objects.create_user(**self.user_info) 53 | user.save() 54 | 55 | # Remove all permissions 56 | UserObjectPermission.objects.filter(user=user).delete() 57 | self.assertEqual(UserObjectPermission.objects.filter(user=user).count(), 58 | 0) 59 | 60 | # Check it 61 | call_command('check_permissions') 62 | 63 | # User should have all permissions again 64 | user_permissions = UserObjectPermission.objects.filter(user=user).values_list('permission__codename', flat=True) 65 | 66 | required_permissions = ['change_user', 'delete_user', 'change_profile', 'view_profile'] 67 | for perm in required_permissions: 68 | if perm not in user_permissions: 69 | self.fail() 70 | 71 | # Check it again should do nothing 72 | call_command('check_permissions', test=True) 73 | 74 | def test_incomplete_permissions(self): 75 | # Delete the neccesary permissions 76 | profile_model_obj = get_profile_model() 77 | content_type_profile = ContentType.objects.get_for_model(profile_model_obj) 78 | content_type_user = ContentType.objects.get_for_model(User) 79 | for model, perms in ASSIGNED_PERMISSIONS.items(): 80 | if model == "profile": 81 | content_type = content_type_profile 82 | else: content_type = content_type_user 83 | for perm in perms: 84 | Permission.objects.get(name=perm[1], 85 | content_type=content_type).delete() 86 | 87 | # Check if they are they are back 88 | for model, perms in ASSIGNED_PERMISSIONS.items(): 89 | if model == "profile": 90 | content_type = content_type_profile 91 | else: content_type = content_type_user 92 | for perm in perms: 93 | try: 94 | perm = Permission.objects.get(name=perm[1], 95 | content_type=content_type) 96 | except Permission.DoesNotExist: pass 97 | else: self.fail("Found %s: " % perm) 98 | 99 | # Repair them 100 | call_command('check_permissions', test=True) 101 | 102 | # Check if they are they are back 103 | for model, perms in ASSIGNED_PERMISSIONS.items(): 104 | if model == "profile": 105 | content_type = content_type_profile 106 | else: content_type = content_type_user 107 | for perm in perms: 108 | try: 109 | perm = Permission.objects.get(name=perm[1], 110 | content_type=content_type) 111 | except Permission.DoesNotExist: 112 | self.fail() 113 | 114 | def test_no_profile(self): 115 | """ Check for warning when there is no profile """ 116 | # TODO: Dirty! Currently we check for the warning by getting a 100% 117 | # test coverage, meaning that it dit output some warning. 118 | user = UserenaSignup.objects.create_user(**self.user_info) 119 | 120 | # remove the profile of this user 121 | get_profile_model().objects.get(user=user).delete() 122 | 123 | # run the command to check for the warning. 124 | call_command('check_permissions', test=True) 125 | 126 | -------------------------------------------------------------------------------- /userena/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib.auth import views as auth_views 3 | 4 | from userena import settings as userena_settings 5 | from userena import views as userena_views 6 | from userena.compat import auth_views_compat_quirks, password_reset_uid_kwarg 7 | 8 | 9 | def merged_dict(dict_a, dict_b): 10 | """Merges two dicts and returns output. It's purpose is to ease use of 11 | ``auth_views_compat_quirks`` 12 | """ 13 | dict_a.update(dict_b) 14 | return dict_a 15 | 16 | urlpatterns = [ 17 | # Signup, signin and signout 18 | url(r'^signup/$', 19 | userena_views.signup, 20 | name='userena_signup'), 21 | url(r'^signin/$', 22 | userena_views.signin, 23 | name='userena_signin'), 24 | url(r'^signout/$', 25 | userena_views.signout, 26 | name='userena_signout'), 27 | 28 | # Reset password 29 | url(r'^password/reset/$', 30 | auth_views.password_reset, 31 | merged_dict({'template_name': 'userena/password_reset_form.html', 32 | 'email_template_name': 'userena/emails/password_reset_message.txt', 33 | 'extra_context': {'without_usernames': userena_settings.USERENA_WITHOUT_USERNAMES} 34 | }, auth_views_compat_quirks['userena_password_reset']), 35 | name='userena_password_reset'), 36 | url(r'^password/reset/done/$', 37 | auth_views.password_reset_done, 38 | {'template_name': 'userena/password_reset_done.html',}, 39 | name='userena_password_reset_done'), 40 | url(r'^password/reset/confirm/(?P<%s>[0-9A-Za-z]+)-(?P.+)/$' % password_reset_uid_kwarg, 41 | auth_views.password_reset_confirm, 42 | merged_dict({'template_name': 'userena/password_reset_confirm_form.html', 43 | }, auth_views_compat_quirks['userena_password_reset_confirm']), 44 | name='userena_password_reset_confirm'), 45 | url(r'^password/reset/confirm/complete/$', 46 | auth_views.password_reset_complete, 47 | {'template_name': 'userena/password_reset_complete.html'}, 48 | name='userena_password_reset_complete'), 49 | 50 | # Signup 51 | url(r'^(?P[\@\.\+\w-]+)/signup/complete/$', 52 | userena_views.direct_to_user_template, 53 | {'template_name': 'userena/signup_complete.html', 54 | 'extra_context': {'userena_activation_required': userena_settings.USERENA_ACTIVATION_REQUIRED, 55 | 'userena_activation_days': userena_settings.USERENA_ACTIVATION_DAYS}}, 56 | name='userena_signup_complete'), 57 | 58 | # Invite 59 | url(r'^invite/$',userena_views.invite_new_user,{'template_name':'userena/invite_new_user.html','success_url':'userena_list_invited_users'}, 60 | name='userena_invite_new_user'), 61 | url(r'^invited-users/$',userena_views.list_invited_users,{'template_name':'userena/list_invited_users.html'},name='userena_list_invited_users'), 62 | url(r'^invite/(?P\w+)/$', 63 | userena_views.activate_invited_user, 64 | name='userena_activate_invited_user'), 65 | 66 | 67 | 68 | # Activate 69 | url(r'^activate/(?P\w+)/$', 70 | userena_views.activate, 71 | name='userena_activate'), 72 | 73 | # Retry activation 74 | url(r'^activate/retry/(?P\w+)/$', 75 | userena_views.activate_retry, 76 | name='userena_activate_retry'), 77 | 78 | # Change email and confirm it 79 | url(r'^(?P[\@\.\+\w-]+)/email/$', 80 | userena_views.email_change, 81 | name='userena_email_change'), 82 | url(r'^(?P[\@\.\+\w-]+)/email/complete/$', 83 | userena_views.direct_to_user_template, 84 | {'template_name': 'userena/email_change_complete.html'}, 85 | name='userena_email_change_complete'), 86 | url(r'^(?P[\@\.\+\w-]+)/confirm-email/complete/$', 87 | userena_views.direct_to_user_template, 88 | {'template_name': 'userena/email_confirm_complete.html'}, 89 | name='userena_email_confirm_complete'), 90 | url(r'^confirm-email/(?P\w+)/$', 91 | userena_views.email_confirm, 92 | name='userena_email_confirm'), 93 | 94 | # Disabled account 95 | url(r'^(?P[\@\.\+\w-]+)/disabled/$', 96 | userena_views.disabled_account, 97 | {'template_name': 'userena/disabled.html'}, 98 | name='userena_disabled'), 99 | 100 | # Change password 101 | url(r'^(?P[\@\.\+\w-]+)/password/$', 102 | userena_views.password_change, 103 | name='userena_password_change'), 104 | url(r'^(?P[\@\.\+\w-]+)/password/complete/$', 105 | userena_views.direct_to_user_template, 106 | {'template_name': 'userena/password_complete.html'}, 107 | name='userena_password_change_complete'), 108 | 109 | # Edit profile 110 | url(r'^(?P[\@\.\+\w-]+)/edit/$', 111 | userena_views.profile_edit, 112 | name='userena_profile_edit'), 113 | 114 | # View profiles 115 | url(r'^(?P(?!(signout|signup|signin)/)[\@\.\+\w-]+)/$', 116 | userena_views.profile_detail, 117 | name='userena_profile_detail'), 118 | url(r'^page/(?P[0-9]+)/$', 119 | userena_views.ProfileListView.as_view(), 120 | name='userena_profile_list_paginated'), 121 | url(r'^$', 122 | userena_views.ProfileListView.as_view(), 123 | name='userena_profile_list'), 124 | ] 125 | -------------------------------------------------------------------------------- /userena/south_migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | from userena.utils import user_model_label 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Adding model 'UserenaSignup' 13 | db.create_table('userena_userenasignup', ( 14 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 15 | ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='userena_signup', unique=True, to=orm[user_model_label])), 16 | ('last_active', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 17 | ('activation_key', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)), 18 | ('activation_notification_send', self.gf('django.db.models.fields.BooleanField')(default=False)), 19 | ('email_unconfirmed', self.gf('django.db.models.fields.EmailField')(max_length=75, blank=True)), 20 | ('email_confirmation_key', self.gf('django.db.models.fields.CharField')(max_length=40, blank=True)), 21 | ('email_confirmation_key_created', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 22 | )) 23 | db.send_create_signal('userena', ['UserenaSignup']) 24 | 25 | 26 | def backwards(self, orm): 27 | 28 | # Deleting model 'UserenaSignup' 29 | db.delete_table('userena_userenasignup') 30 | 31 | 32 | models = { 33 | 'auth.group': { 34 | 'Meta': {'object_name': 'Group'}, 35 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 36 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 37 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 38 | }, 39 | 'auth.permission': { 40 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 41 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 42 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 43 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 44 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 45 | }, 46 | user_model_label: { 47 | 'Meta': {'object_name': user_model_label.split('.')[-1]}, 48 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 49 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 50 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 51 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 52 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 53 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 54 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 55 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 56 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 57 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 58 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 59 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 60 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 61 | }, 62 | 'contenttypes.contenttype': { 63 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 64 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 65 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 67 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 68 | }, 69 | 'userena.userenasignup': { 70 | 'Meta': {'object_name': 'UserenaSignup'}, 71 | 'activation_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 72 | 'activation_notification_send': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 73 | 'email_confirmation_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}), 74 | 'email_confirmation_key_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 75 | 'email_unconfirmed': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 76 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'last_active': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 78 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userena_signup'", 'unique': 'True', 'to': "orm['%s']" % user_model_label}) 79 | } 80 | } 81 | 82 | complete_apps = ['userena'] 83 | --------------------------------------------------------------------------------