views.py 48.5 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

44
from django.shortcuts import get_object_or_404
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
45
from django.contrib import messages
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
46
47
48
49
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
50
51
52
53
from django.http import (
    HttpResponse, HttpResponseBadRequest,
    HttpResponseForbidden, HttpResponseRedirect,
    HttpResponseBadRequest, Http404)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
54
from django.shortcuts import redirect
55
from django.template import RequestContext, loader as template_loader
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
56
57
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
58
from django.views.generic.create_update import (
59
    apply_extra_context, lookup_object, delete_object, get_model_and_form_class)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
60
from django.views.generic.list_detail import object_list, object_detail
61
from django.core.xheaders import populate_xheaders
Olga Brani's avatar
Olga Brani committed
62
from django.core.exceptions import ValidationError, PermissionDenied
63
from django.template.loader import render_to_string
Olga Brani's avatar
Olga Brani committed
64
from django.views.decorators.http import require_http_methods
65
from django.db.models import Q
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
66
from django.core.exceptions import PermissionDenied
67

68
69
import astakos.im.messages as astakos_messages

70
from astakos.im.activation_backends import get_backend, SimpleBackend
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
71
from astakos.im.models import (
72
73
    AstakosUser, ApprovalTerms,
    EmailChange, GroupKind,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
    RESOURCE_SEPARATOR, AstakosUserAuthProvider,
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
75
    PendingThirdPartyUser,
76
77
78
    ProjectApplication, ProjectMembership, Project)
from astakos.im.util import (
    get_context, prepare_response, get_query, restrict_next)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
79
80
81
82
from astakos.im.forms import (
    LoginForm, InvitationForm, ProfileForm,
    FeedbackForm, SignApprovalTermsForm,
    EmailChangeForm,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
83
    ProjectApplicationForm, ProjectSortForm,
84
85
    AddProjectMembersForm, ProjectSearchForm,
    ProjectMembersSortForm)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
86
87
88
89
from astakos.im.functions import (
    send_feedback, SendMailError,
    logout as auth_logout,
    activate as activate_func,
90
    invite,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
91
    send_activation as send_activation_func,
92
93
    SendNotificationError,
    accept_membership, reject_membership, remove_membership,
94
    leave_project, join_project, enroll_member)
95
# from astakos.im.endpoints.qh import timeline_charge
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
96
97
98
from astakos.im.settings import (
    COOKIE_DOMAIN, LOGOUT_NEXT,
    LOGGING_LEVEL, PAGINATE_BY,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
99
100
    RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL,
    MODERATION_ENABLED)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
101
from astakos.im import settings as astakos_settings
root's avatar
root committed
102
#from astakos.im.tasks import request_billing
103
from astakos.im.api.callpoint import AstakosCallpoint
Olga Brani's avatar
Olga Brani committed
104
from astakos.im import auth_providers
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
105
from astakos.im.templatetags.filters import ResourcePresentation
Olga Brani's avatar
Olga Brani committed
106

107
108
logger = logging.getLogger(__name__)

109
callpoint = AstakosCallpoint()
Olga Brani's avatar
Olga Brani committed
110

111
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
112
113
114
115
116
    """
    Calls ``django.template.loader.render_to_string`` with an additional ``tab``
    keyword argument and returns an ``django.http.HttpResponse`` with the
    specified ``status``.
    """
117
    if tab is None:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
118
        tab = template.partition('_')[0].partition('.html')[0]
119
    kwargs.setdefault('tab', tab)
120
    html = template_loader.render_to_string(
121
        template, kwargs, context_instance=context_instance)
122
123
    response = HttpResponse(html, status=status)
    return response
124

Olga Brani's avatar
Olga Brani committed
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def requires_auth_provider(provider_id, **perms):
    """
    """
    def decorator(func, *args, **kwargs):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            provider = auth_providers.get_provider(provider_id)

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

            if provider:
                for pkey, value in perms.iteritems():
                    attr = 'is_available_for_%s' % pkey.lower()
                    if getattr(provider, attr)() != value:
140
                        #TODO: add session message
141
                        return HttpResponseRedirect(reverse('login'))
Olga Brani's avatar
Olga Brani committed
142
143
144
145
            return func(request, *args)
        return wrapper
    return decorator

146
147
148

def requires_anonymous(func):
    """
149
    Decorator checkes whether the request.user is not Anonymous and in that case
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
150
    redirects to `logout`.
151
152
153
154
155
    """
    @wraps(func)
    def wrapper(request, *args):
        if not request.user.is_anonymous():
            next = urlencode({'next': request.build_absolute_uri()})
156
157
            logout_uri = reverse(logout) + '?' + next
            return HttpResponseRedirect(logout_uri)
158
159
160
        return func(request, *args)
    return wrapper

161

162
163
def signed_terms_required(func):
    """
164
    Decorator checks whether the request.user is Anonymous and in that case
165
166
167
168
    redirects to `logout`.
    """
    @wraps(func)
    def wrapper(request, *args, **kwargs):
169
        if request.user.is_authenticated() and not request.user.signed_terms:
170
            params = urlencode({'next': request.build_absolute_uri(),
171
                                'show_form': ''})
172
173
174
175
176
            terms_uri = reverse('latest_terms') + '?' + params
            return HttpResponseRedirect(terms_uri)
        return func(request, *args, **kwargs)
    return wrapper

177

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def required_auth_methods_assigned(only_warn=False):
    """
    Decorator that checks whether the request.user has all required auth providers
    assigned.
    """
    required_providers = auth_providers.REQUIRED_PROVIDERS.keys()

    def decorator(func):
        if not required_providers:
            return func

        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if request.user.is_authenticated():
                for required in required_providers:
                    if not request.user.has_auth_provider(required):
                        provider = auth_providers.get_provider(required)
                        if only_warn:
                            messages.error(request,
                                           _(astakos_messages.AUTH_PROVIDER_REQUIRED  % {
                                               'provider': provider.get_title_display}))
                        else:
                            return HttpResponseRedirect(reverse('edit_profile'))
            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
210
@require_http_methods(["GET", "POST"])
211
@signed_terms_required
212
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
213
    """
214
    If there is logged on user renders the profile page otherwise renders login page.
215

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
216
    **Arguments**
217

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
226
227
    ``extra_context``
        An dictionary of variables to add to the template context.
228

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
229
    **Template:**
230

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

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

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

243
244
245
246
247
    return render_response(
        template_name,
        login_form = LoginForm(request=request),
        context_instance = get_context(request, extra_context)
    )
248

249

Olga Brani's avatar
Olga Brani committed
250
@require_http_methods(["GET", "POST"])
251
@valid_astakos_user_required
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
252
@transaction.commit_manually
253
def invite(request, template_name='im/invitations.html', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
254
255
    """
    Allows a user to invite somebody else.
256

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
257
258
259
    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.
260

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
261
262
    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.
263

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
266
    **Arguments**
267

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
269
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
270
        this will default to ``im/invitations.html``.
271

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
272
273
    ``extra_context``
        An dictionary of variables to add to the template context.
274

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
275
    **Template:**
276

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
279
    **Settings:**
280

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
281
    The view expectes the following settings are defined:
282

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
283
284
    * LOGIN_URL: login uri
    """
285
    extra_context = extra_context or {}
286
287
    status = None
    message = None
288
    form = InvitationForm()
289

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
290
    inviter = request.user
291
    if request.method == 'POST':
292
        form = InvitationForm(request.POST)
293
        if inviter.invitations > 0:
294
295
            if form.is_valid():
                try:
Olga Brani's avatar
Olga Brani committed
296
297
                    email = form.cleaned_data.get('username')
                    realname = form.cleaned_data.get('realname')
298
                    invite(inviter, email, realname)
Olga Brani's avatar
Olga Brani committed
299
                    message = _(astakos_messages.INVITATION_SENT) % locals()
300
                    messages.success(request, message)
301
302
                except SendMailError, e:
                    message = e.message
303
                    messages.error(request, message)
304
                    transaction.rollback()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
305
                except BaseException, e:
Olga Brani's avatar
Olga Brani committed
306
                    message = _(astakos_messages.GENERIC_ERROR)
307
                    messages.error(request, message)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
308
309
310
311
                    logger.exception(e)
                    transaction.rollback()
                else:
                    transaction.commit()
312
        else:
Olga Brani's avatar
Olga Brani committed
313
            message = _(astakos_messages.MAX_INVITATION_NUMBER_REACHED)
314
            messages.error(request, message)
315

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
316
    sent = [{'email': inv.username,
317
318
             'realname': inv.realname,
             'is_consumed': inv.is_consumed}
319
            for inv in request.user.invitations_sent.all()]
320
    kwargs = {'inviter': inviter,
321
              'sent': sent}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
322
323
    context = get_context(request, extra_context, **kwargs)
    return render_response(template_name,
324
325
326
                           invitation_form=form,
                           context_instance=context)

327

Olga Brani's avatar
Olga Brani committed
328
@require_http_methods(["GET", "POST"])
329
@required_auth_methods_assigned(only_warn=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
330
@login_required
331
@signed_terms_required
332
def edit_profile(request, template_name='im/profile.html', extra_context=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
333
334
    """
    Allows a user to edit his/her profile.
335

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
342
    **Arguments**
343

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
344
345
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
346
        this will default to ``im/profile.html``.
347

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
348
349
    ``extra_context``
        An dictionary of variables to add to the template context.
350

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
351
    **Template:**
352

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
355
    **Settings:**
356

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
357
    The view expectes the following settings are defined:
358

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

    # existing providers
    user_providers = request.user.get_active_auth_providers()

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
401
    return render_response(template_name,
402
                           profile_form = form,
Olga Brani's avatar
Olga Brani committed
403
404
                           user_providers = user_providers,
                           user_available_providers = user_available_providers,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
405
                           context_instance = get_context(request,
406
                                                          extra_context))
407

408

409
@transaction.commit_manually
Olga Brani's avatar
Olga Brani committed
410
@require_http_methods(["GET", "POST"])
411
def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
412
413
    """
    Allows a user to create a local account.
414

415
    In case of GET request renders a form for entering the user information.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
416
    In case of POST handles the signup.
417

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
418
    The user activation will be delegated to the backend specified by the ``backend`` keyword argument
419
420
421
    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);
422

423
    Upon successful user creation, if ``next`` url parameter is present the user is redirected there
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
424
    otherwise renders the same page with a success message.
425

426
    On unsuccessful creation, renders ``template_name`` with an error message.
427

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
428
    **Arguments**
429

430
431
    ``template_name``
        A custom template to render. This is optional;
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
432
        if not specified, this will default to ``im/signup.html``.
433

434
435
    ``on_success``
        A custom template to render in case of success. This is optional;
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
436
        if not specified, this will default to ``im/signup_complete.html``.
437

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
438
439
    ``extra_context``
        An dictionary of variables to add to the template context.
440

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
441
    **Template:**
442

443
    im/signup.html or ``template_name`` keyword argument.
444
    im/signup_complete.html or ``on_success`` keyword argument.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
445
    """
446
    extra_context = extra_context or {}
447
    if request.user.is_authenticated():
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
448
        return HttpResponseRedirect(reverse('edit_profile'))
449

450
    provider = get_query(request).get('provider', 'local')
451
452
453
    if not auth_providers.get_provider(provider).is_available_for_create():
        raise PermissionDenied

454
455
456
457
458
459
    id = get_query(request).get('id')
    try:
        instance = AstakosUser.objects.get(id=id) if id else None
    except AstakosUser.DoesNotExist:
        instance = None

460
    third_party_token = request.REQUEST.get('third_party_token', None)
461
462
463
464
465
    if third_party_token:
        pending = get_object_or_404(PendingThirdPartyUser,
                                    token=third_party_token)
        provider = pending.provider
        instance = pending.get_user_instance()
466

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
467
    try:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
468
469
        if not backend:
            backend = get_backend(request)
470
        form = backend.get_signup_form(provider, instance)
471
    except Exception, e:
472
        form = SimpleBackend(request).get_signup_form(provider)
473
        messages.error(request, e)
474
475
    if request.method == 'POST':
        if form.is_valid():
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
476
            user = form.save(commit=False)
477
478
479
            try:
                result = backend.handle_activation(user)
                status = messages.SUCCESS
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
480
                message = result.message
Olga Brani's avatar
Olga Brani committed
481
482
483

                form.store_user(user, request)

484
485
486
487
                if 'additional_email' in form.cleaned_data:
                    additional_email = form.cleaned_data['additional_email']
                    if additional_email != user.email:
                        user.additionalmail_set.create(email=additional_email)
488
                        msg = 'Additional email: %s saved for user %s.' % (
489
490
491
                            additional_email,
                            user.email
                        )
492
                        logger._log(LOGGING_LEVEL, msg, [])
493
494
                if user and user.is_active:
                    next = request.POST.get('next', '')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
495
                    response = prepare_response(request, user, next=next)
496
                    transaction.commit()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
497
                    return response
498
                messages.add_message(request, status, message)
Olga Brani's avatar
Olga Brani committed
499
                transaction.commit()
500
501
502
503
504
505
506
                return render_response(
                    on_success,
                    context_instance=get_context(
                        request,
                        extra_context
                    )
                )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
507
            except SendMailError, e:
508
                logger.exception(e)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
509
510
                status = messages.ERROR
                message = e.message
511
                messages.error(request, message)
512
                transaction.rollback()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
513
            except BaseException, e:
514
                logger.exception(e)
Olga Brani's avatar
Olga Brani committed
515
                message = _(astakos_messages.GENERIC_ERROR)
516
                messages.error(request, message)
517
                logger.exception(e)
518
                transaction.rollback()
519
    return render_response(template_name,
520
                           signup_form=form,
521
                           third_party_token=third_party_token,
522
                           provider=provider,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
523
                           context_instance=get_context(request, extra_context))
524

525

Olga Brani's avatar
Olga Brani committed
526
@require_http_methods(["GET", "POST"])
527
@required_auth_methods_assigned(only_warn=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
528
@login_required
529
@signed_terms_required
530
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
531
532
    """
    Allows a user to send feedback.
533

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
539
    **Arguments**
540

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
541
542
    ``template_name``
        A custom template to use. This is optional; if not specified,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
543
        this will default to ``im/feedback.html``.
544

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
545
546
    ``extra_context``
        An dictionary of variables to add to the template context.
547

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
548
    **Template:**
549

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
552
    **Settings:**
553

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
554
    * LOGIN_URL: login uri
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
555
    """
556
    extra_context = extra_context or {}
557
    if request.method == 'GET':
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
558
559
560
561
        form = FeedbackForm()
    if request.method == 'POST':
        if not request.user:
            return HttpResponse('Unauthorized', status=401)
562

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
563
564
        form = FeedbackForm(request.POST)
        if form.is_valid():
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
565
            msg = form.cleaned_data['feedback_msg']
566
            data = form.cleaned_data['feedback_data']
567
            try:
568
569
                send_feedback(msg, data, request.user, email_template_name)
            except SendMailError, e:
570
                messages.error(request, message)
571
            else:
Olga Brani's avatar
Olga Brani committed
572
                message = _(astakos_messages.FEEDBACK_SENT)
573
                messages.success(request, message)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
574
    return render_response(template_name,
575
576
577
                           feedback_form=form,
                           context_instance=get_context(request, extra_context))

578

579
@require_http_methods(["GET"])
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
580
@signed_terms_required
581
def logout(request, template='registration/logged_out.html', extra_context=None):
582
    """
583
    Wraps `django.contrib.auth.logout`.
584
    """
585
    extra_context = extra_context or {}
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
586
    response = HttpResponse()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
587
588
589
    if request.user.is_authenticated():
        email = request.user.email
        auth_logout(request)
590
591
592
593
594
    else:
        response['Location'] = reverse('index')
        response.status_code = 301
        return response

595
596
597
598
    next = restrict_next(
        request.GET.get('next'),
        domain=COOKIE_DOMAIN
    )
599

600
601
602
    if next:
        response['Location'] = next
        response.status_code = 302
603
604
605
    elif LOGOUT_NEXT:
        response['Location'] = LOGOUT_NEXT
        response.status_code = 301
606
    else:
607
        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
608
609
        response['Location'] = reverse('index')
        response.status_code = 301
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
610
    return response
611

612

Olga Brani's avatar
Olga Brani committed
613
@require_http_methods(["GET", "POST"])
614
@transaction.commit_manually
615
616
def activate(request, greeting_email_template_name='im/welcome_email.txt',
             helpdesk_email_template_name='im/helpdesk_notification.txt'):
617
    """
618
619
    Activates the user identified by the ``auth`` request parameter, sends a welcome email
    and renews the user token.
620

621
622
    The view uses commit_manually decorator in order to ensure the user state will be updated
    only if the email will be send successfully.
623
624
625
626
627
628
    """
    token = request.GET.get('auth')
    next = request.GET.get('next')
    try:
        user = AstakosUser.objects.get(auth_token=token)
    except AstakosUser.DoesNotExist:
629
        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
630

631
    if user.is_active:
Olga Brani's avatar
Olga Brani committed
632
        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
633
        messages.error(request, message)
634
        return index(request)
635

636
    try:
637
        activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
638
639
        response = prepare_response(request, user, next, renew=True)
        transaction.commit()
640
        return response
641
642
643
644
    except SendMailError, e:
        message = e.message
        messages.add_message(request, messages.ERROR, message)
        transaction.rollback()
645
        return index(request)
646
647
    except BaseException, e:
        status = messages.ERROR
648
        message = _(astakos_messages.GENERIC_ERROR)
649
650
651
        messages.add_message(request, messages.ERROR, message)
        logger.exception(e)
        transaction.rollback()
652
        return index(request)
653

654

Olga Brani's avatar
Olga Brani committed
655
@require_http_methods(["GET", "POST"])
656
def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
657
    extra_context = extra_context or {}
658
659
660
661
662
663
664
665
666
    term = None
    terms = None
    if not term_id:
        try:
            term = ApprovalTerms.objects.order_by('-id')[0]
        except IndexError:
            pass
    else:
        try:
667
668
            term = ApprovalTerms.objects.get(id=term_id)
        except ApprovalTerms.DoesNotExist, e:
669
            pass
670

671
    if not term:
Olga Brani's avatar
Olga Brani committed
672
        messages.error(request, _(astakos_messages.NO_APPROVAL_TERMS))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
673
        return HttpResponseRedirect(reverse('index'))
674
675
    f = open(term.location, 'r')
    terms = f.read()
676

677
    if request.method == 'POST':
678
679
680
681
        next = restrict_next(
            request.POST.get('next'),
            domain=COOKIE_DOMAIN
        )
682
        if not next:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
683
            next = reverse('index')
684
685
686
        form = SignApprovalTermsForm(request.POST, instance=request.user)
        if not form.is_valid():
            return render_response(template_name,
687
688
689
                                   terms=terms,
                                   approval_terms_form=form,
                                   context_instance=get_context(request, extra_context))
690
691
692
        user = form.save()
        return HttpResponseRedirect(next)
    else:
693
        form = None
694
        if request.user.is_authenticated() and not request.user.signed_terms:
695
            form = SignApprovalTermsForm(instance=request.user)
696
        return render_response(template_name,
697
698
699
700
                               terms=terms,
                               approval_terms_form=form,
                               context_instance=get_context(request, extra_context))

701

Olga Brani's avatar
Olga Brani committed
702
@require_http_methods(["GET", "POST"])
703
@valid_astakos_user_required
704
705
706
707
708
@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',
709
                 extra_context=None):
710
    extra_context = extra_context or {}
711
712


713
714
715
716
    if activation_key:
        try:
            user = EmailChange.objects.change_email(activation_key)
            if request.user.is_authenticated() and request.user == user:
Olga Brani's avatar
Olga Brani committed
717
                msg = _(astakos_messages.EMAIL_CHANGED)
718
                messages.success(request, msg)
719
720
721
                auth_logout(request)
                response = prepare_response(request, user)
                transaction.commit()
722
                return HttpResponseRedirect(reverse('edit_profile'))
723
        except ValueError, e:
724
            messages.error(request, e)
725
726
727
            transaction.rollback()
            return HttpResponseRedirect(reverse('index'))

728
        return render_response(confirm_template_name,
729
730
                               modified_user=user if 'user' in locals() \
                               else None, context_instance=get_context(request,
731
732
                                                            extra_context))

733
734
    if not request.user.is_authenticated():
        path = quote(request.get_full_path())
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
735
        url = request.build_absolute_uri(reverse('index'))
736
        return HttpResponseRedirect(url + '?next=' + path)
737
738
739
740
741
742
743
744
745

    # clean up expired email changes
    if request.user.email_change_is_pending():
        change = request.user.emailchanges.get()
        if change.activation_key_expired():
            change.delete()
            transaction.commit()
            return HttpResponseRedirect(reverse('email_change'))

746
747
748
    form = EmailChangeForm(request.POST or None)
    if request.method == 'POST' and form.is_valid():
        try:
749
750
            # delete pending email changes
            request.user.emailchanges.all().delete()
751
752
753
            ec = form.save(email_template_name, request)
        except SendMailError, e:
            msg = e
754
            messages.error(request, msg)
755
            transaction.rollback()
756
            return HttpResponseRedirect(reverse('edit_profile'))
757
        else:
Olga Brani's avatar
Olga Brani committed
758
            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
759
            messages.success(request, msg)
760
            transaction.commit()
761
762
763
764
765
            return HttpResponseRedirect(reverse('edit_profile'))

    if request.user.email_change_is_pending():
        messages.warning(request, astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)

766
767
768
769
770
    return render_response(
        form_template_name,
        form=form,
        context_instance=get_context(request, extra_context)
    )
771
772
773


def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
Olga Brani's avatar
Olga Brani committed
774

775
776
777
778
    if request.user.is_authenticated():
        messages.error(request, _(astakos_messages.ALREADY_LOGGED_IN))
        return HttpResponseRedirect(reverse('edit_profile'))

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
779
    if astakos_settings.MODERATION_ENABLED:
Olga Brani's avatar
Olga Brani committed
780
781
        raise PermissionDenied

782
783
784
785
    extra_context = extra_context or {}
    try:
        u = AstakosUser.objects.get(id=user_id)
    except AstakosUser.DoesNotExist:
786
        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
787
788
789
    else:
        try:
            send_activation_func(u)
790
            msg = _(astakos_messages.ACTIVATION_SENT)
791
792
793
794
795
            messages.success(request, msg)
        except SendMailError, e:
            messages.error(request, e)
    return render_response(
        template_name,
796
        login_form = LoginForm(request=request),
797
798
799
800
801
        context_instance = get_context(
            request,
            extra_context
        )
    )
802

803
804