views.py 59.2 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2
#
3
4
5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
6
#
7
8
9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
10
#
11
12
13
14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
15
#
16
17
18
19
20
21
22
23
24
25
26
27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
30
31
32
33
34
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

import logging
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
35
import calendar
Olga Brani's avatar
Olga Brani committed
36
37
38
import inflect

engine = inflect.engine()
39

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
40
from urllib import quote
41
from functools import wraps
42
from datetime import datetime
43
from synnefo.lib.ordereddict import OrderedDict
44

45
46
from django_tables2 import RequestConfig

47
from django.shortcuts import get_object_or_404
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
48
from django.contrib import messages
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
49
50
51
52
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.utils import IntegrityError
53
54
55
56
from django.http import (
    HttpResponse, HttpResponseBadRequest,
    HttpResponseForbidden, HttpResponseRedirect,
    HttpResponseBadRequest, Http404)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
57
from django.shortcuts import redirect
58
from django.template import RequestContext, loader as template_loader
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
59
from django.utils.http import urlencode
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
60
from django.utils.html import escape
61
from django.utils.safestring import mark_safe
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
62
from django.utils.translation import ugettext as _
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
63
from django.views.generic.create_update import (
64
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
65
from django.views.generic.list_detail import object_list, object_detail
66
from django.core.xheaders import populate_xheaders
Olga Brani's avatar
Olga Brani committed
67
from django.core.exceptions import ValidationError, PermissionDenied
68
from django.template.loader import render_to_string
Olga Brani's avatar
Olga Brani committed
69
from django.views.decorators.http import require_http_methods
70
from django.db.models import Q
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
from django.core.exceptions import PermissionDenied
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
72
from django.utils import simplejson as json
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
73
from django.contrib.auth.views import redirect_to_login
74

75
76
import astakos.im.messages as astakos_messages

77
from astakos.im import activation_backends
78
from astakos.im import tables
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
79
from astakos.im.models import (
80
    AstakosUser, ApprovalTerms,
81
    EmailChange, AstakosUserAuthProvider, PendingThirdPartyUser,
82
    ProjectApplication, ProjectMembership, Project, Service, Resource)
83
from astakos.im.util import (
84
    get_context, prepare_response, get_query, restrict_next, model_to_dict)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
85
from astakos.im.forms import (
86
    LoginForm, InvitationForm,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
87
88
    FeedbackForm, SignApprovalTermsForm,
    EmailChangeForm,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
89
    ProjectApplicationForm, ProjectSortForm,
90
91
    AddProjectMembersForm, ProjectSearchForm,
    ProjectMembersSortForm)
92
from astakos.im.forms import ExtendedProfileForm as ProfileForm
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
93
from astakos.im.functions import (
94
    send_feedback,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
95
    logout as auth_logout,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
96
    invite as invite_func,
97
    qh_add_pending_app,
98
    accept_membership, reject_membership, remove_membership, cancel_membership,
99
100
    leave_project, join_project, enroll_member, can_join_request,
    can_leave_request,
101
102
103
    get_related_project_id, get_by_chain_or_404,
    approve_application, deny_application,
    cancel_application, dismiss_application)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
104
105
106
from astakos.im.settings import (
    COOKIE_DOMAIN, LOGOUT_NEXT,
    LOGGING_LEVEL, PAGINATE_BY,
107
    PAGINATE_BY_ALL,
108
    ACTIVATION_REDIRECT_URL,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
109
    MODERATION_ENABLED)
110
from astakos.im import presentation
111
from astakos.im import settings
112
from astakos.im import auth_providers as auth
113
from snf_django.lib.db.transaction import commit_on_success_strict
114
from astakos.im.ctx import ExceptionHandler
115
from astakos.im import quotas
Olga Brani's avatar
Olga Brani committed
116

117
118
logger = logging.getLogger(__name__)

Olga Brani's avatar
Olga Brani committed
119
120


121
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
122
123
124
125
126
    """
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
    keyword argument and returns an ``django.http.HttpResponse`` with the
    specified ``status``.
    """
127
    if tab is None:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
128
        tab = template.partition('_')[0].partition('.html')[0]
129
    kwargs.setdefault('tab', tab)
130
    html = template_loader.render_to_string(
131
        template, kwargs, context_instance=context_instance)
132
133
    response = HttpResponse(html, status=status)
    return response
134

Olga Brani's avatar
Olga Brani committed
135
136
137
138
139
140
def requires_auth_provider(provider_id, **perms):
    """
    """
    def decorator(func, *args, **kwargs):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
141
            provider = auth.get_provider(provider_id)
Olga Brani's avatar
Olga Brani committed
142
143
144
145

            if not provider or not provider.is_active():
                raise PermissionDenied

146
147
148
149
150
            for pkey, value in perms.iteritems():
                attr = 'get_%s_policy' % pkey.lower()
                if getattr(provider, attr) != value:
                    #TODO: add session message
                    return HttpResponseRedirect(reverse('login'))
Olga Brani's avatar
Olga Brani committed
151
152
153
154
            return func(request, *args)
        return wrapper
    return decorator

155
156
157

def requires_anonymous(func):
    """
158
    Decorator checkes whether the request.user is not Anonymous and in that case
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
159
    redirects to `logout`.
160
161
162
163
164
    """
    @wraps(func)
    def wrapper(request, *args):
        if not request.user.is_anonymous():
            next = urlencode({'next': request.build_absolute_uri()})
165
166
            logout_uri = reverse(logout) + '?' + next
            return HttpResponseRedirect(logout_uri)
167
168
169
        return func(request, *args)
    return wrapper

170

171
172
def signed_terms_required(func):
    """
173
    Decorator checks whether the request.user is Anonymous and in that case
174
175
176
177
    redirects to `logout`.
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
178
        if request.user.is_authenticated() and not request.user.signed_terms:
179
            params = urlencode({'next': request.build_absolute_uri(),
180
                                'show_form': ''})
181
182
183
184
185
            terms_uri = reverse('latest_terms') + '?' + params
            return HttpResponseRedirect(terms_uri)
        return func(request, *args, **kwargs)
    return wrapper

186

187
def required_auth_methods_assigned(allow_access=False):
188
189
190
191
192
193
194
195
196
    """
    Decorator that checks whether the request.user has all required auth providers
    assigned.
    """

    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if request.user.is_authenticated():
197
198
199
200
201
202
203
                missing = request.user.missing_required_providers()
                if missing:
                    for provider in missing:
                        messages.error(request,
                                       provider.get_required_msg)
                    if not allow_access:
                        return HttpResponseRedirect(reverse('edit_profile'))
204
205
206
207
208
209
210
211
212
            return func(request, *args, **kwargs)
        return wrapper
    return decorator


def valid_astakos_user_required(func):
    return signed_terms_required(required_auth_methods_assigned()(login_required(func)))


Olga Brani's avatar
Olga Brani committed
213
@require_http_methods(["GET", "POST"])
214
@signed_terms_required
215
def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
216
    """
217
    If there is logged on user renders the profile page otherwise renders login page.
218

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
219
    **Arguments**
220

221
222
    ``login_template_name``
        A custom login template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
223
        this will default to ``im/login.html``.
224

225
226
    ``profile_template_name``
        A custom profile template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
227
        this will default to ``im/profile.html``.
228

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
230
    ``extra_context``
        An dictionary of variables to add to the template context.
231

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
232
    **Template:**
233

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
    im/profile.html or im/login.html or ``template_name`` keyword argument.
235

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
236
    """
237
    extra_context = extra_context or {}
238
239
    template_name = login_template_name
    if request.user.is_authenticated():
240
        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
Olga Brani's avatar
Olga Brani committed
241

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
242
243
244
245
    third_party_token = request.GET.get('key', False)
    if third_party_token:
        messages.info(request, astakos_messages.AUTH_PROVIDER_LOGIN_TO_ADD)

246
247
248
249
250
    return render_response(
        template_name,
        login_form = LoginForm(request=request),
        context_instance = get_context(request, extra_context)
    )
251

252

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
253
254
255
256
257
258
259
260
261
262
263
264
265
@require_http_methods(["POST"])
@valid_astakos_user_required
def update_token(request):
    """
    Update api token view.
    """
    user = request.user
    user.renew_token()
    user.save()
    messages.success(request, astakos_messages.TOKEN_UPDATED)
    return HttpResponseRedirect(reverse('edit_profile'))


Olga Brani's avatar
Olga Brani committed
266
@require_http_methods(["GET", "POST"])
267
@valid_astakos_user_required
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
@transaction.commit_manually
269
def invite(request, template_name='im/invitations.html', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
271
    """
    Allows a user to invite somebody else.
272

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
273
274
275
    In case of GET request renders a form for providing the invitee information.
    In case of POST checks whether the user has not run out of invitations and then
    sends an invitation email to singup to the service.
276

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
277
278
    The view uses commit_manually decorator in order to ensure the number of the
    user invitations is going to be updated only if the email has been successfully sent.
279

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
280
    If the user isn't logged in, redirects to settings.LOGIN_URL.
281

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
282
    **Arguments**
283

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
284
285
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
286
        this will default to ``im/invitations.html``.
287

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
288
289
    ``extra_context``
        An dictionary of variables to add to the template context.
290

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
291
    **Template:**
292

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
293
    im/invitations.html or ``template_name`` keyword argument.
294

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
295
    **Settings:**
296

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
297
    The view expectes the following settings are defined:
298

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
299
300
    * LOGIN_URL: login uri
    """
301
    extra_context = extra_context or {}
302
303
    status = None
    message = None
304
    form = InvitationForm()
305

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
306
    inviter = request.user
307
    if request.method == 'POST':
308
        form = InvitationForm(request.POST)
309
        if inviter.invitations > 0:
310
311
            if form.is_valid():
                try:
Olga Brani's avatar
Olga Brani committed
312
313
                    email = form.cleaned_data.get('username')
                    realname = form.cleaned_data.get('realname')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
314
                    invite_func(inviter, email, realname)
Olga Brani's avatar
Olga Brani committed
315
                    message = _(astakos_messages.INVITATION_SENT) % locals()
316
                    messages.success(request, message)
317
                except Exception, e:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
318
                    transaction.rollback()
319
                    raise
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
320
321
                else:
                    transaction.commit()
322
        else:
Olga Brani's avatar
Olga Brani committed
323
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
324
            messages.error(request, message)
325

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
326
    sent = [{'email': inv.username,
327
328
             'realname': inv.realname,
             'is_consumed': inv.is_consumed}
329
            for inv in request.user.invitations_sent.all()]
330
    kwargs = {'inviter': inviter,
331
              'sent': sent}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
332
333
    context = get_context(request, extra_context, **kwargs)
    return render_response(template_name,
334
335
336
                           invitation_form=form,
                           context_instance=context)

337

Olga Brani's avatar
Olga Brani committed
338
@require_http_methods(["GET", "POST"])
339
@required_auth_methods_assigned(allow_access=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
340
@login_required
341
@signed_terms_required
342
def edit_profile(request, template_name='im/profile.html', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
343
344
    """
    Allows a user to edit his/her profile.
345

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
346
    In case of GET request renders a form for displaying the user information.
347
348
    In case of POST updates the user informantion and redirects to ``next``
    url parameter if exists.
349

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
350
    If the user isn't logged in, redirects to settings.LOGIN_URL.
351

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
352
    **Arguments**
353

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
354
355
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
356
        this will default to ``im/profile.html``.
357

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
358
359
    ``extra_context``
        An dictionary of variables to add to the template context.
360

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
361
    **Template:**
362

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
363
    im/profile.html or ``template_name`` keyword argument.
364

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
365
    **Settings:**
366

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
367
    The view expectes the following settings are defined:
368

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
369
    * LOGIN_URL: login uri
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
370
    """
371
    extra_context = extra_context or {}
372
373
374
375
    form = ProfileForm(
        instance=request.user,
        session_key=request.session.session_key
    )
376
    extra_context['next'] = request.GET.get('next')
377
    if request.method == 'POST':
378
379
380
381
382
        form = ProfileForm(
            request.POST,
            instance=request.user,
            session_key=request.session.session_key
        )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
383
        if form.is_valid():
384
            try:
385
                prev_token = request.user.auth_token
386
                user = form.save(request=request)
387
388
389
390
                next = restrict_next(
                    request.POST.get('next'),
                    domain=COOKIE_DOMAIN
                )
Olga Brani's avatar
Olga Brani committed
391
                msg = _(astakos_messages.PROFILE_UPDATED)
392
                messages.success(request, msg)
393
394
395
396
397
398
399
400

                if form.email_changed:
                    msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
                    messages.success(request, msg)
                if form.password_changed:
                    msg = _(astakos_messages.PASSWORD_CHANGED)
                    messages.success(request, msg)

401
402
403
404
                if next:
                    return redirect(next)
                else:
                    return redirect(reverse('edit_profile'))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
405
            except ValueError, ve:
406
                messages.success(request, ve)
407
    elif request.method == "GET":
Olga Brani's avatar
Olga Brani committed
408
409
410
411
        request.user.is_verified = True
        request.user.save()

    # existing providers
412
413
    user_providers = request.user.get_enabled_auth_providers()
    user_disabled_providers = request.user.get_disabled_auth_providers()
Olga Brani's avatar
Olga Brani committed
414
415
416
417

    # providers that user can add
    user_available_providers = request.user.get_available_auth_providers()

418
    extra_context['services'] = Service.catalog().values()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
419
    return render_response(template_name,
420
421
422
423
424
                           profile_form=form,
                           user_providers=user_providers,
                           user_disabled_providers=user_disabled_providers,
                           user_available_providers=user_available_providers,
                           context_instance=get_context(request,
425
                                                          extra_context))
426

427

428
@transaction.commit_manually
Olga Brani's avatar
Olga Brani committed
429
@require_http_methods(["GET", "POST"])
430
431
def signup(request, template_name='im/signup.html', on_success='index',
           extra_context=None, activation_backend=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
432
433
    """
    Allows a user to create a local account.
434

435
    In case of GET request renders a form for entering the user information.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
436
    In case of POST handles the signup.
437

438
439
440
441
442
443
    The user activation will be delegated to the backend specified by the
    ``activation_backend`` keyword argument if present, otherwise to the
    ``astakos.im.activation_backends.InvitationBackend`` if
    settings.ASTAKOS_INVITATIONS_ENABLED is True or
    ``astakos.im.activation_backends.SimpleBackend`` if not (see
    activation_backends);
444

445
446
447
    Upon successful user creation, if ``next`` url parameter is present the
    user is redirected there otherwise renders the same page with a success
    message.
448

449
    On unsuccessful creation, renders ``template_name`` with an error message.
450

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
451
    **Arguments**
452

453
454
    ``template_name``
        A custom template to render. This is optional;
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
455
        if not specified, this will default to ``im/signup.html``.
456

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
457
458
    ``extra_context``
        An dictionary of variables to add to the template context.
459

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
460
461
462
    ``on_success``
        Resolvable view name to redirect on registration success.

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
463
    **Template:**
464

465
    im/signup.html or ``template_name`` keyword argument.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
466
    """
467
    extra_context = extra_context or {}
468
    if request.user.is_authenticated():
469
470
471
        logger.info("%s already signed in, redirect to index",
                    request.user.log_display)
        return HttpResponseRedirect(reverse('index'))
472

473
    provider = get_query(request).get('provider', 'local')
474
    if not auth.get_provider(provider).get_create_policy:
475
        logger.error("%s provider not available for signup", provider)
476
477
        raise PermissionDenied

478
    instance = None
479

480
    # user registered using third party provider
481
    third_party_token = request.REQUEST.get('third_party_token', None)
482
    unverified = None
483
    if third_party_token:
484
485
        # retreive third party entry. This was created right after the initial
        # third party provider handshake.
486
487
        pending = get_object_or_404(PendingThirdPartyUser,
                                    token=third_party_token)
488

489
        provider = pending.provider
490
491

        # clone third party instance into the corresponding AstakosUser
492
        instance = pending.get_user_instance()
493
        get_unverified = AstakosUserAuthProvider.objects.unverified
494
495

        # check existing unverified entries
496
497
498
499
500
        unverified = get_unverified(pending.provider,
                                    identifier=pending.third_party_identifier)

        if unverified and request.method == 'GET':
            messages.warning(request, unverified.get_pending_registration_msg)
501
            if unverified.user.moderated:
502
503
504
505
506
                messages.warning(request,
                                 unverified.get_pending_resend_activation_msg)
            else:
                messages.warning(request,
                                 unverified.get_pending_moderation_msg)
507

508
509
510
511
512
513
514
515
516
517
    # prepare activation backend based on current request
    if not activation_backend:
        activation_backend = activation_backends.get_backend()

    form_kwargs = {'instance': instance}
    if third_party_token:
        form_kwargs['third_party_token'] = third_party_token

    form = activation_backend.get_signup_form(
        provider, None, **form_kwargs)
518

519
    if request.method == 'POST':
520
521
522
523
524
        form = activation_backend.get_signup_form(
            provider,
            request.POST,
            **form_kwargs)

525
        if form.is_valid():
526
527
528
            commited = False
            try:
                user = form.save(commit=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
529

530
531
532
                # delete previously unverified accounts
                if AstakosUser.objects.user_exists(user.email):
                    AstakosUser.objects.get_by_identifier(user.email).delete()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
533

534
                # store_user so that user auth providers get initialized
535
                form.store_user(user, request)
536
537
538
539
540
541
542
                result = activation_backend.handle_registration(user)
                if result.status == \
                        activation_backend.Result.PENDING_MODERATION:
                    # user should be warned that his account is not active yet
                    status = messages.WARNING
                else:
                    status = messages.SUCCESS
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
543
                message = result.message
544
                activation_backend.send_result_notifications(result, user)
Olga Brani's avatar
Olga Brani committed
545

546
547
548
549
550
                # commit user entry
                transaction.commit()
                # commited flag
                # in case an exception get raised from this point
                commited = True
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
551

552
                if user and user.is_active:
553
554
                    # activation backend directly activated the user
                    # log him in
555
                    next = request.POST.get('next', '')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
556
557
                    response = prepare_response(request, user, next=next)
                    return response
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
558
559
560

                messages.add_message(request, status, message)
                return HttpResponseRedirect(reverse(on_success))
561
562
563
564
            except Exception, e:
                if not commited:
                    transaction.rollback()
                raise
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
565

566
    return render_response(template_name,
567
                           signup_form=form,
568
                           third_party_token=third_party_token,
569
                           provider=provider,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
570
                           context_instance=get_context(request, extra_context))
571

572

Olga Brani's avatar
Olga Brani committed
573
@require_http_methods(["GET", "POST"])
574
@required_auth_methods_assigned(allow_access=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
575
@login_required
576
@signed_terms_required
577
def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
578
579
    """
    Allows a user to send feedback.
580

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
581
582
    In case of GET request renders a form for providing the feedback information.
    In case of POST sends an email to support team.
583

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
584
    If the user isn't logged in, redirects to settings.LOGIN_URL.
585

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
586
    **Arguments**
587

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
588
589
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
590
        this will default to ``im/feedback.html``.
591

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
592
593
    ``extra_context``
        An dictionary of variables to add to the template context.
594

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
595
    **Template:**
596

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
597
    im/signup.html or ``template_name`` keyword argument.
598

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
599
    **Settings:**
600

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
601
    * LOGIN_URL: login uri
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
602
    """
603
    extra_context = extra_context or {}
604
    if request.method == 'GET':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
605
606
607
608
        form = FeedbackForm()
    if request.method == 'POST':
        if not request.user:
            return HttpResponse('Unauthorized', status=401)
609

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
610
611
        form = FeedbackForm(request.POST)
        if form.is_valid():
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
612
            msg = form.cleaned_data['feedback_msg']
613
            data = form.cleaned_data['feedback_data']
614
615
616
            send_feedback(msg, data, request.user, email_template_name)
            message = _(astakos_messages.FEEDBACK_SENT)
            messages.success(request, message)
617
            return HttpResponseRedirect(reverse('feedback'))
618

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
619
    return render_response(template_name,
620
                           feedback_form=form,
621
622
                           context_instance=get_context(request,
                                                        extra_context))
623

624

625
@require_http_methods(["GET"])
626
627
def logout(request, template='registration/logged_out.html',
           extra_context=None):
628
    """
629
    Wraps `django.contrib.auth.logout`.
630
    """
631
    extra_context = extra_context or {}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
632
    response = HttpResponse()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
633
634
635
    if request.user.is_authenticated():
        email = request.user.email
        auth_logout(request)
636
637
638
639
640
    else:
        response['Location'] = reverse('index')
        response.status_code = 301
        return response

641
642
643
644
    next = restrict_next(
        request.GET.get('next'),
        domain=COOKIE_DOMAIN
    )
645

646
647
648
    if next:
        response['Location'] = next
        response.status_code = 302
649
650
651
    elif LOGOUT_NEXT:
        response['Location'] = LOGOUT_NEXT
        response.status_code = 301
652
    else:
653
654
655
656
657
658
        last_provider = request.COOKIES.get('astakos_last_login_method', 'local')
        provider = auth.get_provider(last_provider)
        message = provider.get_logout_success_msg
        extra = provider.get_logout_success_extra_msg
        if extra:
            message += "<br />"  + extra
659
        messages.success(request, message)
660
661
        response['Location'] = reverse('index')
        response.status_code = 301
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
662
    return response
663

664

Olga Brani's avatar
Olga Brani committed
665
@require_http_methods(["GET", "POST"])
666
@transaction.commit_manually
667
668
def activate(request, greeting_email_template_name='im/welcome_email.txt',
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
669
    """
670
671
    Activates the user identified by the ``auth`` request parameter, sends a
    welcome email and renews the user token.
672

673
674
    The view uses commit_manually decorator in order to ensure the user state
    will be updated only if the email will be send successfully.
675
676
677
    """
    token = request.GET.get('auth')
    next = request.GET.get('next')
678

679
680
    if request.user.is_authenticated():
        message = _(astakos_messages.LOGGED_IN_WARNING)
681
        messages.error(request, message)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
682
        return HttpResponseRedirect(reverse('index'))
683

684
685
686
687
688
689
690
    try:
        user = AstakosUser.objects.get(verification_code=token)
    except AstakosUser.DoesNotExist:
        raise Http404

    if user.email_verified:
        message = _(astakos_messages.ACCOUNT_ALREADY_VERIFIED)
691
692
693
        messages.error(request, message)
        return HttpResponseRedirect(reverse('index'))

694
    try:
695
696
697
        backend = activation_backends.get_backend()
        result = backend.handle_verification(user, token)
        backend.send_result_notifications(result, user)
698
        next = ACTIVATION_REDIRECT_URL or next
699
700
701
702
703
704
705
706
707
708
        response = HttpResponseRedirect(reverse('index'))
        if user.is_active:
            response = prepare_response(request, user, next, renew=True)
            messages.success(request, _(result.message))
        else:
            messages.warning(request, _(result.message))
    except Exception:
        transaction.rollback()
        raise
    else:
709
        transaction.commit()
710
        return response
711

712

Olga Brani's avatar
Olga Brani committed
713
@require_http_methods(["GET", "POST"])
714
715
def approval_terms(request, term_id=None,
                   template_name='im/approval_terms.html', extra_context=None):
716
    extra_context = extra_context or {}
717
718
719
720
721
722
723
724
725
    term = None
    terms = None
    if not term_id:
        try:
            term = ApprovalTerms.objects.order_by('-id')[0]
        except IndexError:
            pass
    else:
        try:
726
727
            term = ApprovalTerms.objects.get(id=term_id)
        except ApprovalTerms.DoesNotExist, e:
728
            pass
729

730
    if not term:
Olga Brani's avatar
Olga Brani committed
731
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
732
        return HttpResponseRedirect(reverse('index'))
733
734
735
736
737
    try:
        f = open(term.location, 'r')
    except IOError:
        messages.error(request, _(astakos_messages.GENERIC_ERROR))
        return render_response(
738
739
            template_name, context_instance=get_context(request,
                                                        extra_context))
740

741
    terms = f.read()
742

743
    if request.method == 'POST':
744
745
746
747
        next = restrict_next(
            request.POST.get('next'),
            domain=COOKIE_DOMAIN
        )
748
        if not next:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
749
            next = reverse('index')
750
751
752
        form = SignApprovalTermsForm(request.POST, instance=request.user)
        if not form.is_valid():
            return render_response(template_name,
753
754
                                   terms=terms,
                                   approval_terms_form=form,
755
756
                                   context_instance=get_context(request,
                                                                extra_context))
757
758
759
        user = form.save()
        return HttpResponseRedirect(next)
    else:
760
        form = None
761
        if request.user.is_authenticated() and not request.user.signed_terms:
762
            form = SignApprovalTermsForm(instance=request.user)
763
        return render_response(template_name,
764
765
                               terms=terms,
                               approval_terms_form=form,
766
767
                               context_instance=get_context(request,
                                                            extra_context))
768

769

Olga Brani's avatar
Olga Brani committed
770
@require_http_methods(["GET", "POST"])
771
772
773
774
775
@transaction.commit_manually
def change_email(request, activation_key=None,
                 email_template_name='registration/email_change_email.txt',
                 form_template_name='registration/email_change_form.html',
                 confirm_template_name='registration/email_change_done.html',
776
                 extra_context=None):
777
    extra_context = extra_context or {}
778

779
    if not settings.EMAILCHANGE_ENABLED:
780
781
        raise PermissionDenied

782
783
    if activation_key:
        try:
784
785
786
787
788
789
790
791
792
793
794
            try:
                email_change = EmailChange.objects.get(
                    activation_key=activation_key)
            except EmailChange.DoesNotExist:
                transaction.rollback()
                logger.error("[change-email] Invalid or used activation "
                             "code, %s", activation_key)
                raise Http404

            if (request.user.is_authenticated() and \
                request.user == email_change.user) or not \
795
                    request.user.is_authenticated():
796
                user = EmailChange.objects.change_email(activation_key)
Olga Brani's avatar
Olga Brani committed
797
                msg = _(astakos_messages.EMAIL_CHANGED)
798
                messages.success(request, msg)
799
                transaction.commit()
800
                return HttpResponseRedirect(reverse('edit_profile'))
801
802
803
804
805
            else:
                logger.error("[change-email] Access from invalid user, %s %s",
                             email_change.user, request.user.log_display)
                transaction.rollback()
                raise PermissionDenied
806
        except ValueError, e:
807
            messages.error(request, e)
808
809
810
            transaction.rollback()
            return HttpResponseRedirect(reverse('index'))

811
        return render_response(confirm_template_name,
812
                               modified_user=user if 'user' in locals()
813
                               else None, context_instance=get_context(request,
814
                               extra_context))
815

Sofia Papagiannaki's avatar