From e2444785cdb1252ca6762cc4d6c34f8c69309d6d Mon Sep 17 00:00:00 2001 From: philomat Date: Sun, 21 Aug 2011 22:04:14 -0400 Subject: [PATCH] Finalized docs for release --- doc/activation.rst | 111 ++++++++++++++++++ doc/configuration.rst | 24 +++- doc/dummy/models.py | 4 + doc/dummy/settings.py | 4 + doc/forms.rst | 7 +- doc/index.rst | 9 +- doc/installation.rst | 38 ++++++ doc/managers.rst | 5 +- doc/middleware.rst | 6 +- doc/models.rst | 2 + doc/multiple-profiles.rst | 26 +++- doc/other.rst | 5 +- doc/overview.rst | 4 +- doc/signals.rst | 10 ++ doc/templatetags.rst | 2 - doc/views.rst | 3 + user_profiles/activation/models.py | 8 ++ user_profiles/activation/urls.py | 4 +- user_profiles/activation/utils.py | 39 +++++- user_profiles/activation/views.py | 33 +++++- user_profiles/forms.py | 69 +++++++---- user_profiles/managers.py | 61 +++++++++- user_profiles/models.py | 20 +++- user_profiles/settings.py | 18 +++ user_profiles/signals.py | 6 + .../templatetags/user_profile_tags.py | 12 ++ user_profiles/urls.py | 3 - user_profiles/utils.py | 13 +- user_profiles/views.py | 88 +++++++++----- 29 files changed, 541 insertions(+), 93 deletions(-) create mode 100644 doc/dummy/models.py create mode 100644 doc/signals.rst delete mode 100644 doc/templatetags.rst diff --git a/doc/activation.rst b/doc/activation.rst index efd4c78..ebd96ba 100644 --- a/doc/activation.rst +++ b/doc/activation.rst @@ -1,2 +1,113 @@ +.. _activation: + User Activation *************** + +Many web applications require some form of user activation, usually to verify +user-provided information such as email addresses. django-user-profiles provides +an activation module, implemented as a Django application. + + +Installation +============ + +- In order to enable activation, add the ``activation`` module to your + ``INSTALLED_APPS`` setting:: + + INSTALLED_APPS = ( + # ... your other apps here + 'user_profiles', + 'user_profiles.activation', + ) + +- Include the activation URLconf in your ``urls.py``:: + + urlpatterns = patterns('', + # ... your urls here + (r'^user/', include('user_profiles.activation.urls')), + ) + +.. note:: + Whenever new user sign up, they will now be sent an activation request via + email. However, if you want to prevent new users from logging in unless they + have activated, you need to set :ref:`USER_PROFILES_USER_SET_ACTIVE_ON_SIGNUP + ` to ``False`` in your project's + settings module. Sometimes you also need to re-request activation from users, + for instance when they change their email address. See + :ref:`activation-utility` for more information. + +This application automatically connects to the ``post_signup`` signal dispatched +by django-user-profiles in order send to request activation from new users. +See the :ref:`signals documentation ` for more +information. + + +.. _activation-templates: + +Message templates +================= + +For rendering the activation request email, text templates are used. You are +likely to require your own customized messages. To customize the email text, +simply your own templates with the following names: + +``activation/email/activation_request.subject.txt`` + The subject line of the activation email. + +``activation/email/activation_request.txt`` + The body text of the activation email. This should display the activation + key to the user, plus a link to the activation page and form. Example template:: + + Dear {{ recipient }}, please go to {{ url }} to activate your account. + If the above link doesn't work, go to {{ form_url }} and enter the + following key: {{ key }} + + +The following context variables are available to both of these templates: + +``url`` + The activation link URL + +``form_url`` + The link to the activation form + +``site_url`` + The link to the website that requires activation + +``site`` + The corresponding ``Site`` object + +``key`` + The activation key + +``user`` + The associated ``User`` object + +``recipient`` + An object that resolves to a string containing the name of the user (i.e. + either the user or the profile object, depending on whether the profile + model has a ``__unicode__`` method). + +``profile`` + The associated user profile object. + +``created`` + If the user was just created, this will ``True``. If the user already + existed, it will be ``False``. You are probably going to want to address + users in a different way if the just signed up. + + +Views +===== + +.. automodule:: user_profiles.activation.views + :members: + +.. _activation-utility: + +Utility functions +================= + +.. autofunction:: user_profiles.activation.utils.require_activation_from_user + +.. autofunction:: user_profiles.activation.utils.accept_activation_code diff --git a/doc/configuration.rst b/doc/configuration.rst index 761432a..44a310e 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -1,3 +1,5 @@ +.. _configuration: + Configuring django-user-profiles ******************************** @@ -26,13 +28,15 @@ USER_PROFILES_URL_FIELD The field of the user profile model used to generate URLs to profile pages. You could replace this by a slug field, for instance. +.. _user-profiles-user-set-active-on-signup: + USER_PROFILES_USER_SET_ACTIVE_ON_SIGNUP Default: ``True`` Specifies whether the ``is_active`` field of a user object should be set to ``True`` on signup, immediately enabling the user to log in. If this is disabled, further activation is necessary, for instance using the activation - module provided by django-user-profiles. + module provided by this application. See :ref:`activation`. USER_PROFILES_EMAIL_AS_USERNAME @@ -42,3 +46,21 @@ USER_PROFILES_EMAIL_AS_USERNAME enabled, email addresses and usernames will be kept synchronized, and users log in with their email address instead of specifying a username. +USER_PROFILES_PUBLIC + Default: ``False`` + + Specifies whether all user profiles should be visible to any user, including + anonymous visitors. Set this to ``False`` if users should not be able to see + the information of other users. + + .. warning:: + Making user profiles public could pose serious privacy and security + violations. + +USER_PROFILES_PUBLIC_WHEN_LOGGED_IN + Default: ``False`` + + Specifies whether all user profiles should be visible to any logged-in user. + Set this to ``False`` if users should not be able to see the information of + other users. + diff --git a/doc/dummy/models.py b/doc/dummy/models.py new file mode 100644 index 0000000..6c029b4 --- /dev/null +++ b/doc/dummy/models.py @@ -0,0 +1,4 @@ +from django.db import models + +class Profile(models.Model): + pass \ No newline at end of file diff --git a/doc/dummy/settings.py b/doc/dummy/settings.py index b2721d0..9b73bf9 100644 --- a/doc/dummy/settings.py +++ b/doc/dummy/settings.py @@ -1,5 +1,7 @@ # Django settings for dummy project. +IS_SPHINX_BUILD_DUMMY = True + DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -143,3 +145,5 @@ }, } } + +AUTH_PROFILE_MODULE = 'dummy.Profile' \ No newline at end of file diff --git a/doc/forms.rst b/doc/forms.rst index 3d0ddec..dc51894 100644 --- a/doc/forms.rst +++ b/doc/forms.rst @@ -1,2 +1,7 @@ +.. _forms: + User Forms -********** \ No newline at end of file +********** + +.. automodule:: user_profiles.forms + :members: diff --git a/doc/index.rst b/doc/index.rst index 8280011..cef1696 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,8 +6,8 @@ Introduction django-user-profiles is a flexible app that wires together Django's user authentication and user profile features, with customizable forms and models. -Furthermore, it provides a layer for user profile activation, plus support for a -multiple-profiles-per-user feature. +Furthermore, it provides a module for user profile activation, plus basic +support for a multiple-profiles-per-user feature. Table of Contents @@ -18,13 +18,14 @@ Table of Contents overview installation + configuration models + managers views - configuration activation multiple-profiles forms - templatetags + signals other diff --git a/doc/installation.rst b/doc/installation.rst index d2b3aa6..5ea2a52 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -1,2 +1,40 @@ Installing django-user-profiles ******************************* + +.. note:: + Please refer to the Django documentation on `storing additional information + about users + `_ + for more information about how Django handles user profiles. + +- Create a user profile model subclassing + ``user_profiles.models.UserProfileBase``, and define it in your project + settings (see :ref:`models`):: + + AUTH_PROFILE_MODULE = 'my_app.MyUserProfile' + +- Add the ``user_profiles`` module to your ``INSTALLED_APPS`` setting:: + + INSTALLED_APPS = ( + # ... your other apps here + 'user_profiles', + ) + +- Add ``user_profiles.middleware.CurrentUserMiddleware`` to your ``MIDDLEWARE_CLASSES``:: + + MIDDLEWARE_CLASSES = ( + # ... your other middleware classes here + 'user_profiles.middleware.CurrentUserMiddleware', + ) + + +- Include the URLconf in your ``urls.py``:: + + urlpatterns = patterns('', + # ... your urls here + (r'^user/', include('user_profiles.urls')), + ) + +- If you want to enable user activation, you also need to install that module. + See :ref:`activation`. + diff --git a/doc/managers.rst b/doc/managers.rst index 8bc6133..e0c32c0 100644 --- a/doc/managers.rst +++ b/doc/managers.rst @@ -1,2 +1,5 @@ Model Managers -************** \ No newline at end of file +************** + +.. automodule:: user_profiles.managers + :members: \ No newline at end of file diff --git a/doc/middleware.rst b/doc/middleware.rst index 9fe7ade..6a3db6b 100644 --- a/doc/middleware.rst +++ b/doc/middleware.rst @@ -1,5 +1,9 @@ +.. _middleware: + Middleware ********** .. automodule:: user_profiles.middleware - :members: \ No newline at end of file + :members: + + \ No newline at end of file diff --git a/doc/models.rst b/doc/models.rst index eac2443..d8363b5 100644 --- a/doc/models.rst +++ b/doc/models.rst @@ -1,3 +1,5 @@ +.. _models: + User Profile Models ******************* diff --git a/doc/multiple-profiles.rst b/doc/multiple-profiles.rst index 75852fc..c689c90 100644 --- a/doc/multiple-profiles.rst +++ b/doc/multiple-profiles.rst @@ -1,2 +1,26 @@ Implementing a multiple-profiles-per-user feature -************************************************* \ No newline at end of file +************************************************* + +This application provides very basic support for implementing a web site where +one user can have several profiles (as in different sets of contact +information), with one being their default profile that is returned by Django's +``user.get_profile()`` method. This is achieved by filtering profile objects +by the ``is_default`` field. Please see the :ref:`user profile base model +` for more information on this subject. + +Currently, django-user-profiles comes without any views for creating, +displaying, updating or deleting additional profiles (although this might change +in the future). You are responsible for creating such views yourself. + +These views can be pretty standard, following the basic CRUD concept as seen in +most Django applications. You can use the standard model form provided by +django-user-profiles, and of course you need to make sure that users can only +edit their own profiles. + +.. warning:: + If you are implementing a multiple-profiles-per-user feature in your project, + you should prevent users from deleting their default profile by checking + first whether ``is_default`` is ``True``. When creating additonal profiles + for a user, you also need to make sure that the ``is_default`` field is + ``True`` for exactly one profile object so as not to create any ambiguities + that lead to errors. diff --git a/doc/other.rst b/doc/other.rst index e3787fe..67532bf 100644 --- a/doc/other.rst +++ b/doc/other.rst @@ -1,11 +1,10 @@ -Secondary features -****************** +Secondary features and fixes +**************************** .. toctree:: :maxdepth: 2 middleware admin - managers patches \ No newline at end of file diff --git a/doc/overview.rst b/doc/overview.rst index 870ed98..b5501ab 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -3,8 +3,8 @@ Overview django-user-profiles is a flexible app that wires together Django's user authentication and user profile features, with customizable forms and models. -Furthermore, it provides a layer for user profile activation, plus support for a -multiple-profiles-per-user feature. +Furthermore, it provides a module for user profile activation, plus basic +support for a multiple-profiles-per-user feature. This app also aims to offer some improvements where the standard ``django.contrib.auth`` app falls short. diff --git a/doc/signals.rst b/doc/signals.rst new file mode 100644 index 0000000..2c06beb --- /dev/null +++ b/doc/signals.rst @@ -0,0 +1,10 @@ +.. _user-profiles-signals: + +Signals +******* + +django-user-profiles provides the following signals that you can connect to if +you need specific code to be executed on certain events. + +.. automodule:: user_profiles.signals + :members: diff --git a/doc/templatetags.rst b/doc/templatetags.rst deleted file mode 100644 index c2b04aa..0000000 --- a/doc/templatetags.rst +++ /dev/null @@ -1,2 +0,0 @@ -Template Tags -************* diff --git a/doc/views.rst b/doc/views.rst index 0b61c86..fe3a283 100644 --- a/doc/views.rst +++ b/doc/views.rst @@ -1,2 +1,5 @@ User Views ********** + +.. automodule:: user_profiles.views + :members: diff --git a/user_profiles/activation/models.py b/user_profiles/activation/models.py index bbac8a6..f2af7f7 100644 --- a/user_profiles/activation/models.py +++ b/user_profiles/activation/models.py @@ -6,6 +6,10 @@ import uuid class ActivationCode(models.Model): + """ + An ``ActivationCode`` object is a key and user pair. If the accurate key + is provided, the user can be activated. + """ key = models.CharField(max_length=32, editable=False) user = models.ForeignKey(User, editable=False) activated = models.BooleanField(editable=False, default=False) @@ -18,6 +22,10 @@ def save(self, *args, **kwargs): super(ActivationCode, self).save(*args, **kwargs) def post_signup_send_activation_link_to_new_user(sender, **kwargs): + """ + This is the signal handler for the ``post_signup`` signal dispatched by + the user_profiles app. It sends an activation request to the new user. + """ from user_profiles.activation.utils import send_activation_link_to_user send_activation_link_to_user(kwargs['user'], created=True) diff --git a/user_profiles/activation/urls.py b/user_profiles/activation/urls.py index 4fb01a8..316f853 100644 --- a/user_profiles/activation/urls.py +++ b/user_profiles/activation/urls.py @@ -1,8 +1,8 @@ from django.conf.urls.defaults import * urlpatterns = patterns('', - url(r'^activation/send/$', 'user_profiles.activation.views.current_user_send', name='user_profiles_activation_current_user_send'), - url(r'^activation/([a-z0-9]+)/$', 'user_profiles.activation.views.activate', name='user_profiles_activation_activate'), + url(r'^activation/send/$', 'user_profiles.activation.views.send_activation_code_to_user', name='user_profiles_send_activation_code_to_user'), + url(r'^activation/([a-z0-9]+)/$', 'user_profiles.activation.views.activate', name='user_profiles_activate'), url(r'^activation/$', 'user_profiles.activation.views.activate', name='user_profiles_activation_form'), ) diff --git a/user_profiles/activation/utils.py b/user_profiles/activation/utils.py index f97201a..3d08678 100644 --- a/user_profiles/activation/utils.py +++ b/user_profiles/activation/utils.py @@ -8,6 +8,22 @@ from django.contrib.sites.models import Site def require_activation_from_user(user, activation_code=None, set_user_inactive=False, profile_instance=None, created=False): + """ + Deactivates a user account and requests activation from the user. You can + call this function to re-request activation of a previously activated user + account, for instance when users change their email address. To achieve + this, you would typically write a handler for the ``User`` object's + ``post_save`` signal. See the `relevant Django documentation + `_. + + + .. note:: + If your user profile model has a ``deactivate`` method, it will be called + by this function. Hence, if you need any specific code to be executed for + deactivation of the user profile, you can simply implement its + ``deactivate`` method. + + """ if set_user_inactive: user.is_active = False user.save() @@ -17,6 +33,16 @@ def require_activation_from_user(user, activation_code=None, set_user_inactive=F send_activation_link_to_user(user, activation_code, created) def accept_activation_code(activation_code): + """ + Marks an ``ActivationCode`` object as used, and re-activates the associated + user account. + + .. note:: + If your user profile model has an ``activate`` method, it will be called + by this function. Hence, if you need any specific code to be executed for + activation of the user profile, you can simply implement its ``activate`` + method. + """ activation_code.user.is_active = True activation_code.user.save() user_profile = activation_code.user.get_profile() @@ -26,6 +52,11 @@ def accept_activation_code(activation_code): activation_code.save() def send_activation_link_to_user(user, activation_code=None, created=False): + """ + Sends (and creates, if necessary) an activation code to the user passed. The + ``created`` argument should be ``True`` if that user was just created. + See :ref:_activation-templates. + """ if not activation_code: ActivationCode.objects.filter(user=user, activated=False).delete() activation_code = ActivationCode(user=user) @@ -42,7 +73,7 @@ def send_activation_link_to_user(user, activation_code=None, created=False): else: recipient = user context_dict = { - 'url': qualified_url(reverse('user_profiles_activation_activate', args=[activation_code.key]), site), + 'url': qualified_url(reverse('user_profiles_activate', args=[activation_code.key]), site), 'form_url': qualified_url(reverse('user_profiles_activation_form'), site), 'site_url': qualified_url('', site), 'site': site, @@ -52,8 +83,10 @@ def send_activation_link_to_user(user, activation_code=None, created=False): 'profile': profile, 'created': created, } - subject = render_message('activation/email/activation_request.subject.txt', context_dict, remove_newlines=True) - message = render_message('activation/email/activation_request.txt', context_dict) + subject = render_message('activation/email/activation_request.subject.txt', + context_dict, remove_newlines=True) + message = render_message('activation/email/activation_request.txt', + context_dict) if app_settings.BY_EMAIL: send_mail(subject, message, None, [user.email], fail_silently=False) diff --git a/user_profiles/activation/views.py b/user_profiles/activation/views.py index a224e63..d3e11a2 100644 --- a/user_profiles/activation/views.py +++ b/user_profiles/activation/views.py @@ -1,3 +1,9 @@ +""" +The views provided by the ``activation`` module will be available in your +project if you included the URLconf as explained in the installation +instructions. +""" + from user_profiles.activation.models import ActivationCode from user_profiles.activation.utils import require_activation_from_user, accept_activation_code from user_profiles.activation.signals import post_activation @@ -16,6 +22,15 @@ class ActivationForm(forms.Form): key = forms.CharField(label=_('Activation code'), required=True) def activate(request, key=None): + """ + Tries to activate a user account with the key passed, and redirects users to + their profile page on success. + + If no key or an invalid key was passed, the activation form will be + rendered. Usually, users won't see that form since they are going to click + the activation link in their email. However, as a backup they can also enter + the key manually using this form. + """ if request.method == 'POST': form = ActivationForm(request.POST) key = request.POST.get('key', None) @@ -49,14 +64,24 @@ def activate(request, key=None): return render_to_response('activation/form.html', {'form': form}, context_instance=RequestContext(request)) @login_required -def current_user_send(request): +def send_activation_code_to_user(request, user=None): + """ + Sends an activation code to the user passed (or the current user, if + omitted). If no activation code exists for this user, there will be one + created. You can use this view if users need to be able to re-request their + activation code, for instance when a previous email did not arrive. + """ + if not user: + user = request.user try: - activation_code = ActivationCode.objects.filter(user=request.user, activated=False)[0] + # Use existing activation code + activation_code = ActivationCode.objects.filter(user=user, activated=False)[0] except IndexError: + # If none exists, require_activation_from_user will create one activation_code = None - require_activation_from_user(request.user, activation_code) + require_activation_from_user(user, activation_code) success_message = _('An activation code has been sent to your email address: %(email)s. Please click the link in the email in order to activate.') % { - 'email': request.user.email + 'email': user.email } messages.success(request, success_message) return HttpResponseRedirect(request.META.get('HTTP_REFERER', settings.LOGIN_REDIRECT_URL)) diff --git a/user_profiles/forms.py b/user_profiles/forms.py index 121f8cb..0e360f2 100644 --- a/user_profiles/forms.py +++ b/user_profiles/forms.py @@ -1,3 +1,8 @@ +""" +You can subclass any of the following form classes if you need to customize your +signup, login or profile editing processes. +""" + from user_profiles import settings as app_settings from user_profiles.utils import get_user_profile_model from django.db.models import Q @@ -8,7 +13,9 @@ class AuthenticationForm(ContribAuthAuthenticationForm): - + """ + The basic login form, based on Django's default authentication form. + """ def __init__(self, *args, **kwargs): super(AuthenticationForm, self).__init__(*args, **kwargs) if app_settings.EMAIL_AS_USERNAME: @@ -20,12 +27,41 @@ def __init__(self, *args, **kwargs): class ProfileForm(forms.ModelForm): + """ + The basic user profile form. This is simply a ``ModelForm`` for the user + profile model. + """ class Meta: model = get_user_profile_model() class SignupForm(UserCreationForm): + """ + The basic signup form, based on Django's ``UserCreationForm`` form. - email = forms.EmailField(required=True, label=_('E-mail address'), help_text=_('Your e-mail address is your username. You need to provide a valid address to log in.')) + Signup forms are different from user profile forms in that you might want to + keep it as simple as possible, i.e. require just the most basic information + during signup, in order not to overwhelm the user. + + This form's ``save()`` method handles a few extra tasks, such as creating + initially inactive user accounts if you configured your project + appropriately. See :ref:`activation`. + """ + + email = forms.EmailField(required=True, label=_('E-mail address')) + + def __init__(self, *args, **kwargs): + super(SignupForm, self).__init__(*args, **kwargs) + if app_settings.EMAIL_AS_USERNAME: + del self.fields['username'] + self.fields.insert(0, 'email', self.fields.pop('email')) + self.fields['email'].help_text = _('Your e-mail address is your username. You need to provide a valid address to log in.') + else: + self.fields['email'].help_text = _('You need to provide a valid address.') + + def clean_email(self): + if User.objects.filter(Q(email=self.cleaned_data['email']) | Q(username=self.cleaned_data['email'])).exists(): + raise forms.ValidationError(_('A user with this e-mail address already exists.')) + return self.cleaned_data['email'] def save(self, commit=True): from user_profiles import settings as app_settings @@ -36,8 +72,12 @@ def save(self, commit=True): return user -# TODO not working -- maybe inheritance is not the best way +# TODO not working -- maybe inheritance is not the way to do it class SignupWithProfileForm(SignupForm, ProfileForm): + """ + NOT IMPLEMENTED. Signup form requiring users to fill in their full profile + during signup. + """ class Meta: model = get_user_profile_model() @@ -45,26 +85,3 @@ class Meta: def save(self, *args, **kwargs): return SignupForm(self.cleaned_data).save(*args, **kwargs) -# TODO remove and replace with EMAIL_AS_USERNAME setting -class EmailAsUsernameSignupForm(SignupForm): - email = forms.EmailField(required=True, label=_('E-mail address'), help_text=_('Your e-mail address is your username. You need to provide a valid address to log in.')) - - class Meta: - model = User - fields = ("email",) - - def __init__(self, *args, **kwargs): - super(EmailAsUsernameSignupForm, self).__init__(*args, **kwargs) - del(self.fields['username']) - - def clean_email(self): - if User.objects.filter(Q(email=self.cleaned_data['email']) | Q(username=self.cleaned_data['email'])).exists(): - raise forms.ValidationError(_('A user with this e-mail address already exists.')) - return self.cleaned_data['email'] - - def save(self, commit=True): - user = super(EmailAsUsernameSignupForm, self).save(commit=False) - user.username = user.email - if commit: - user.save() - return user diff --git a/user_profiles/managers.py b/user_profiles/managers.py index 5d74c53..6a0c7c2 100644 --- a/user_profiles/managers.py +++ b/user_profiles/managers.py @@ -1,17 +1,70 @@ +""" +django-user-profiles includes a few model managers for commonly used tasks, for +instance to retrieve all objects created by a specific user. +""" + from user_profiles.middleware import CurrentUserMiddleware from django.db import models -class CreatedByCurrentUserManager(models.Manager): - # TODO make field configurable +class ByUserManager(models.Manager): + """ + A manager for accessing all objects created by a specific user. + + You can pass the name of the field that is storing the user when creating an + instance of this class. If omitted, the default field is ``created_by``. + + Example usage:: + + class MyModel(models.Model): + created_by = models.ForeignKey(User) + objects = ByUserManager() + + With this model, ``MyModel.objects.all()`` will return all objects, while + ``MyModel.objects.by_user(some_user)`` will return all objects created by + the specified user, which is a handy shortcut instead of using the more + verbose ``MyModel.objects.filter(user=some_user)``. + """ + + user_field = 'created_by' + + def __init__(self, user_field=None): + super(ByUserManager, self).__init__() + if user_field: + self.user_field = user_field + + def by_user(self, user): + return super(ByUserManager, self).get_query_set().filter(**{ + self.user_field: user}) + + +class ByCurrentUserManager(ByUserManager): + """ + A manager for accessing all objects created by the current user, based on + ``CurrentUserMiddleware`` (see :ref:`middleware`). + + You can pass the name of the field that is storing the user when creating an + instance of this class. If omitted, the default field is ``created_by``. + + Example usage:: + + class MyModel(models.Model): + created_by = models.ForeignKey(User) + by_current_user = ByCurrentUserManager() + + With this model, ``MyModel.objects.all()`` will return all objects, while + ``MyModel.by_current_user.all()`` will return all objects created by the + current user. + """ + def get_query_set(self): user = CurrentUserMiddleware.get_current_user() if not user or not user.is_authenticated(): user = None - return super(CreatedByCurrentUserManager, self).get_query_set().filter(created_by=user) + return self.by_user(user) -class UserDefaultProfileManager(models.Manager): +class UserDefaultProfileManager(ByUserManager): def get_query_set(self): return super(UserDefaultProfileManager, self).get_query_set().filter(is_default=True) diff --git a/user_profiles/models.py b/user_profiles/models.py index 92a3af7..9f188ec 100644 --- a/user_profiles/models.py +++ b/user_profiles/models.py @@ -7,7 +7,7 @@ from user_profiles import settings as app_settings from user_profiles.utils import get_user_profile_model, sync_profile_fields -from user_profiles.managers import UserDefaultProfileManager, CreatedByCurrentUserManager +from user_profiles.managers import UserDefaultProfileManager, ByUserManager, ByCurrentUserManager from django.db import models from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User @@ -75,10 +75,14 @@ class UserProfileBase(models.Model): An abstract base class for your custom user profile model, defining a few commonly used methods, as well as the field :attr:`is_default` which enables you to implement a multiple-profiles-per-user feature. - + That field is a boolean specifying whether a specific instance is a user's default profile and thus will be returned by that user's ``get_profile()`` method. + + .. note:: + If in your project every user only has one profile, you can just ignore + the ``is_default`` field. When you subclass this class, the default ``Manager`` of your custom user profile model (:attr:`objects`) will only return instances whose @@ -94,10 +98,14 @@ class UserProfileBase(models.Model): ``ModelAdmin``, since site administrators would typically need access to all profiles instead of just one individual user's profiles. - .. note:: + .. warning:: If you are implementing a multiple-profiles-per-user feature in your project, you should prevent users from deleting their default profile by - checking first whether ``is_default`` is ``True``. + checking first whether ``is_default`` is ``True``. When creating + additonal profiles for a user, you also need to make sure that the + ``is_default`` field is ``True`` for exactly one profile object so as not + to create any ambiguities that lead to errors. + """ is_default = models.BooleanField(_('is default profile'), editable=False, default=False) @@ -120,7 +128,7 @@ class Meta: information on how to access user's secondary profiles. """ - by_current_user = CreatedByCurrentUserManager() + by_current_user = ByCurrentUserManager() """ A ``Manager`` instance providing access to the user profile instances created by the currently logged-in user only. @@ -156,6 +164,8 @@ def queryset(self, request, queryset): """ + by_user = ByUserManager().by_user + def __init__(self, *args, **kwargs): super(UserProfileBase, self).__init__(*args, **kwargs) # As explained in docstring of this class: ``is_default`` attribute diff --git a/user_profiles/settings.py b/user_profiles/settings.py index 3e5410f..1a53882 100644 --- a/user_profiles/settings.py +++ b/user_profiles/settings.py @@ -47,3 +47,21 @@ enabled, email addresses and usernames will be kept synchronized, and users log in with their email address instead of specifying a username. """ + +PUBLIC = getattr(settings, 'USER_PROFILES_PUBLIC', False) +""" +Default: ``False`` + +Specifies whether all user profiles should be visible to any user, including +anonymous visitors. Set this to ``False`` if users should not be able to see +the information of other users. +""" + +PUBLIC_WHEN_LOGGED_IN = getattr(settings, 'USER_PROFILES_PUBLIC_WHEN_LOGGED_IN', False) +""" +Default: ``False`` + +Specifies whether all user profiles should be visible to any logged-in user. +Set this to ``False`` if users should not be able to see the information of +other users. +""" \ No newline at end of file diff --git a/user_profiles/signals.py b/user_profiles/signals.py index 5051a6c..d97724d 100644 --- a/user_profiles/signals.py +++ b/user_profiles/signals.py @@ -10,6 +10,12 @@ respective ``User`` instance. The ``activation`` module uses this signal to send users a confirmation email. You can connect to this signal if you need to execute custom code after users sign up. + +Arguments sent with this signal: + +``user`` + The user object that was just created. + """ # for internal use only diff --git a/user_profiles/templatetags/user_profile_tags.py b/user_profiles/templatetags/user_profile_tags.py index f0f0964..54f093a 100644 --- a/user_profiles/templatetags/user_profile_tags.py +++ b/user_profiles/templatetags/user_profile_tags.py @@ -2,6 +2,18 @@ from django.utils.translation import ugettext_lazy as _ def salutation(title, last_name, first_name=''): + """ + Returns a localizable concatenation of first and last name, and the + appropriate salutation according to title. + + Possible values for title: + ``MR``, ``MS``, ``FAMILY`` or empty string + + Example:: + >>> salutation('MR', 'Duck', 'Donald') + Dear Mr Duck + + """ values = {'first_name': first_name, 'last_name': last_name} if title == 'MRS': return _('Dear Mrs %(last_name)s,') % values diff --git a/user_profiles/urls.py b/user_profiles/urls.py index 6dda485..2b924b6 100644 --- a/user_profiles/urls.py +++ b/user_profiles/urls.py @@ -21,7 +21,4 @@ #url(r'^$', 'user_profiles.views.redirect_to_current_user_detail'), ] -if 'user_profiles.activation' in settings.INSTALLED_APPS: - pat.append(url(r'^', include('user_profiles.activation.urls'))) - urlpatterns = patterns('', *pat) diff --git a/user_profiles/utils.py b/user_profiles/utils.py index 15b384a..f46dd27 100644 --- a/user_profiles/utils.py +++ b/user_profiles/utils.py @@ -5,6 +5,7 @@ from django.template.loader import get_template from django.template import Context from django.utils.text import normalize_newlines +from django.conf import settings _user_profile_model_cache = None @@ -29,7 +30,9 @@ def get_user_profile_model(): try: model = models.get_model(app_label, model_name) - if model is None: + # There is an issue with sphinx_build not being able to import the + # profile model. Simply ignore the error in this case. + if model is None and not getattr(settings, 'IS_SPHINX_BUILD_DUMMY', False): raise SiteProfileNotAvailable('Unable to load the profile ' 'model, check AUTH_PROFILE_MODULE in your project sett' 'ings') @@ -85,8 +88,12 @@ def getattr_field_lookup(obj, lookup): obj = manager.all()[0] return getattr(obj, attr) -def render_message(template, context_dict, remove_newlines=False): - message = get_template(template).render(Context(context_dict, autoescape=False)) +def render_message(template_name, context_dict, remove_newlines=False): + """ + Shortcut method for rendering message templates, such as the ones used for + activation emails. + """ + message = get_template(template_name).render(Context(context_dict, autoescape=False)) if remove_newlines: message = normalize_newlines(message).replace('\n', '') return message diff --git a/user_profiles/views.py b/user_profiles/views.py index 91a2167..3eca287 100644 --- a/user_profiles/views.py +++ b/user_profiles/views.py @@ -32,7 +32,14 @@ SIGNUP_FORM_CLASS = get_class_from_path(app_settings.SIGNUP_FORM) PROFILE_FORM_CLASS = get_class_from_path(app_settings.PROFILE_FORM) -def signup(request): +def signup(request, template_name='user_profiles/signup.html'): + """ + The signup view, creating a new ``User`` object on POST requests and + rendering the signup form otherwise. + + See :ref:`configuration` and :ref:`forms` for information on how to use + your own custom form. + """ if request.user.is_authenticated(): return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) if request.method == 'POST': @@ -59,32 +66,47 @@ def signup(request): context_dict = { 'form': signup_form } - return render_to_response('user_profiles/signup.html', + return render_to_response(template_name, context_dict, context_instance=RequestContext(request)) -def _user_detail(request, user, extra_context={}): - context_dict = { - 'user' : user, - 'profile': user.get_profile(), - } - context_dict.update(extra_context) - return render_to_response('user_profiles/profile/detail.html', - context_dict, context_instance=RequestContext(request)) +def _user_detail(request, user, template_name, + extra_context={}): + context_dict = { + 'user' : user, + 'profile': user.get_profile(), + } + context_dict.update(extra_context) + return render_to_response(template_name, + context_dict, context_instance=RequestContext(request)) -@login_required -def user_detail(request, lookup_value, extra_context={}): - kwargs = {app_settings.URL_FIELD: lookup_value} - try: - user = User.objects.get(**kwargs) - return _user_detail(request, user, extra_context) - except (User.DoesNotExist, ValueError): - raise Http404 +def user_detail(request, lookup_value, template_name='user_profiles/profile/detail.html', + extra_context={}): + """ + Renders the public detail/profile page for a given user. + + By default, public profiles are disabled for privacy reasons. See + :ref:`configuration` for information on how to enable them. + """ + if not app_settings.PUBLIC and not \ + (app_settings.PUBLIC_WHEN_LOGGED_IN and request.user.is_authenticated()) and not \ + (request.user.is_staff and request.user.has_perm('auth.change_user')): + raise PermissionDenied() + kwargs = {app_settings.URL_FIELD: lookup_value} + try: + user = User.objects.get(**kwargs) + return _user_detail(request, user, template_name, extra_context) + except (User.DoesNotExist, ValueError): + raise Http404 @login_required -def current_user_detail(request, extra_context={}): - return _user_detail(request, request.user, extra_context) +def current_user_detail(request, template_name='user_profiles/profile/detail.html', + extra_context={}): + """ + Renders the detail/profile page for the currently logged-in user. + """ + return _user_detail(request, request.user, template_name, extra_context) -def _user_change(request, user): +def _user_change(request, user, template_name): profile_model = get_user_profile_model() if request.method == 'POST': try: @@ -114,21 +136,33 @@ def _user_change(request, user): 'profile' : profile, } - return render_to_response('user_profiles/profile/change.html', + return render_to_response(template_name, context_dict, context_instance=RequestContext(request)) - def logout_then_login(request, **kwargs): + """ + Wraps the standard view for logging out and logging in, additionally + creating a user message. + """ result = auth_views.logout_then_login(request, **kwargs) messages.info(request, _('You have been logged out.')) return result @login_required -def current_user_profile_change(request): - return _user_change(request, request.user) +def current_user_profile_change(request, template_name='user_profiles/profile/change.html'): + """ + The profile edit view for the currently logged in user. + + See :ref:`configuration` and :ref:`forms` for information on how to use + your own custom form. + """ + return _user_change(request, request.user, template_name) @login_required def redirect_to_current_user_detail(request): + """ + Redirects to the detail/profile page of the currently logged-in user. + """ return HttpResponseRedirect(reverse('current_user_detail')) @csrf_protect @@ -137,7 +171,7 @@ def password_change(request, **kwargs): """ Wraps the standard view for changing passwords provided by ``contrib.auth``, redirecting to the user profile page with a user message when done instead - of showing an intermediary page that is sometimes useless. + of showing an intermediary page. """ if not kwargs.get('post_change_redirect'): kwargs['post_change_redirect'] = reverse('current_user_detail') @@ -151,7 +185,7 @@ def password_reset_confirm(request, **kwargs): """ Wraps the standart password reset view provided by ``contrib.auth`` , redirecting to the login page with a user message when done instead - of showing an intermediary page that is sometimes useless. + of showing an intermediary page. """ if not kwargs.get('post_reset_redirect'): kwargs['post_reset_redirect'] = reverse('login')