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
45
from django_tables2 import RequestConfig

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

70
71
import astakos.im.messages as astakos_messages

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

109
110
logger = logging.getLogger(__name__)

111
callpoint = AstakosCallpoint()
Olga Brani's avatar
Olga Brani committed
112

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

Olga Brani's avatar
Olga Brani committed
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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:
142
                        #TODO: add session message
143
                        return HttpResponseRedirect(reverse('login'))
Olga Brani's avatar
Olga Brani committed
144
145
146
147
            return func(request, *args)
        return wrapper
    return decorator

148
149
150

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

163

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

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
210
211
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
212
@require_http_methods(["GET", "POST"])
213
@signed_terms_required
214
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
215
    """
216
    If there is logged on user renders the profile page otherwise renders login page.
217

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

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

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

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

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

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

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

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

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

251

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

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
268
    **Arguments**
269

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
277
    **Template:**
278

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
281
    **Settings:**
282

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
283
    The view expectes the following settings are defined:
284

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

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

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

329

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
344
    **Arguments**
345

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
353
    **Template:**
354

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
357
    **Settings:**
358

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
359
    The view expectes the following settings are defined:
360

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
361
    * LOGIN_URL: login uri
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
362
    """
363
    extra_context = extra_context or {}
364
365
366
367
    form = ProfileForm(
        instance=request.user,
        session_key=request.session.session_key
    )
368
    extra_context['next'] = request.GET.get('next')
369
    if request.method == 'POST':
370
371
372
373
374
        form = ProfileForm(
            request.POST,
            instance=request.user,
            session_key=request.session.session_key
        )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
375
        if form.is_valid():
376
            try:
377
378
                prev_token = request.user.auth_token
                user = form.save()
379
380
381
382
                form = ProfileForm(
                    instance=user,
                    session_key=request.session.session_key
                )
383
384
385
386
                next = restrict_next(
                    request.POST.get('next'),
                    domain=COOKIE_DOMAIN
                )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
387
388
                if next:
                    return redirect(next)
Olga Brani's avatar
Olga Brani committed
389
                msg = _(astakos_messages.PROFILE_UPDATED)
390
                messages.success(request, msg)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
391
            except ValueError, ve:
392
                messages.success(request, ve)
393
    elif request.method == "GET":
Olga Brani's avatar
Olga Brani committed
394
395
396
397
398
399
400
401
402
        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
403
    return render_response(template_name,
404
                           profile_form = form,
Olga Brani's avatar
Olga Brani committed
405
406
                           user_providers = user_providers,
                           user_available_providers = user_available_providers,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
407
                           context_instance = get_context(request,
408
                                                          extra_context))
409

410

411
@transaction.commit_manually
Olga Brani's avatar
Olga Brani committed
412
@require_http_methods(["GET", "POST"])
413
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
414
415
    """
    Allows a user to create a local account.
416

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

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

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

428
    On unsuccessful creation, renders ``template_name`` with an error message.
429

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
430
    **Arguments**
431

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
443
    **Template:**
444

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

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

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

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

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

                form.store_user(user, request)

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

527

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

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
541
    **Arguments**
542

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

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
550
    **Template:**
551

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
554
    **Settings:**
555

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

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

580

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

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

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

614

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

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

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

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

656

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

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

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

703

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


715
716
717
718
    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
719
                msg = _(astakos_messages.EMAIL_CHANGED)
720
                messages.success(request, msg)
721
722
723
                auth_logout(request)
                response = prepare_response(request, user)
                transaction.commit()
724
                return HttpResponseRedirect(reverse('edit_profile'))
725
        except ValueError, e:
726
            messages.error(request, e)
727
728
729
            transaction.rollback()
            return HttpResponseRedirect(reverse('index'))

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

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

    # 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'))

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

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

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


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

777
778
779
780
    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
781
    if astakos_settings.MODERATION_ENABLED:
Olga Brani's avatar
Olga Brani committed
782
783
        raise PermissionDenied

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