views.py 14.5 KB
Newer Older
1 2 3 4
"""
go/views.py
"""

David Haynes's avatar
David Haynes committed
5 6 7
# Python stdlib imports
from datetime import timedelta

8
# Django Imports
9
from django.conf import settings
10 11 12 13
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied  # ValidationError
from django.core.mail import EmailMessage, send_mail
14
from django.http import HttpResponseServerError  # Http404
15
from django.http import HttpResponseRedirect
16
from django.shortcuts import get_object_or_404, redirect, render
17
from django.utils import timezone
David Haynes's avatar
David Haynes committed
18 19

# Other imports
20
from ratelimit.decorators import ratelimit
21

22
# App Imports
23
from .forms import URLForm, EditForm
24
from .models import URL, RegisteredUser
25

26

David Haynes's avatar
David Haynes committed
27
def index(request):
28 29 30 31 32 33
    """
    If a user is logged in, this view displays all the information about all
    of their URLs. Otherwise, it will show the public landing page
    """

    # If the user is not authenticated, show them a public landing page.
34
    if not request.user.is_authenticated:
35 36 37
        return render(request, 'public_landing.html')

    # Get the current domain info
38
    domain = "%ss://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
39 40 41 42

    # Grab a list of all the URL's that are currently owned by the user
    urls = URL.objects.filter(owner=request.user.registereduser)

43
    # Render my_links passing the list of URL's and Domain to the template
44 45 46 47
    return render(request, 'core/index.html', {
        'urls': urls,
        'domain': domain,
    })
48

49
@login_required
Zach Knox's avatar
Zach Knox committed
50
def new_link(request):
51
    """
David Haynes's avatar
David Haynes committed
52
    This view handles the homepage that the user is presented with when
53
    they request '/newLink'. If they're not logged in, they're redirected to
54
    login.
55 56
    """

57 58 59
    # If the user is blocked, then display the you're blocked page.
    if request.user.registereduser.blocked:
        return render(request, 'banned.html')
60

David Haynes's avatar
David Haynes committed
61
    # Initialize a URL form
62
    url_form = URLForm(host=request.META.get('HTTP_HOST'))  # unbound form
63

David Haynes's avatar
David Haynes committed
64 65
    # If a POST request is received, then the user has submitted a form and it's
    # time to parse the form and create a new URL object
66
    if request.method == 'POST':
67 68
        # Now we initialize the form again but this time we have the POST
        # request
69
        return _new_link_post(request)
70

71 72 73 74
    # Render index.html passing the form to the template
    return render(request, 'core/new_link.html', {
        'form': url_form,
    })
75

76 77 78 79 80
def _new_link_post(request):
    """
    This view handles when a POST is received from the new_link form.
    """
    url_form = URLForm(request.POST, host=request.META.get('HTTP_HOST'))
81

82 83 84 85 86 87
    if not url_form.is_valid():
        # there is an error, redisplay the form with the validation errors
        # Render index.html passing the form to the template
        return render(request, 'core/new_link.html', {
            'form': url_form,
        })
88

89 90
    # Call our post method to assemble our new URL object
    res = post(request, url_form)
91

92 93 94
    # If there is a 500 error returned, handle it
    if res == 500:
        return HttpResponseServerError(render(request, '500.html'))
95

96 97
    # Redirect to the shiny new URL
    return redirect('view', res.short)
Jean Michel Rouly's avatar
Jean Michel Rouly committed
98

99
@login_required
Zach Knox's avatar
Zach Knox committed
100
def my_links(request):
101 102 103 104 105
    """
    for compatibility, just in case
    shows the same thing as /, but requires login to be consistent with
    /newLink
    """
106 107
    if request.user.registereduser.blocked:
        return render(request, 'banned.html')
Zach Knox's avatar
Zach Knox committed
108 109
    return index(request)

David Haynes's avatar
David Haynes committed
110
# Rate limits are completely arbitrary
111 112
@ratelimit(key='user', rate='3/m', method='POST', block=True)
@ratelimit(key='user', rate='25/d', method='POST', block=True)
113
def post(request, url_form):
114
    """
115
    Helper function that handles POST requests for the URL creation
116 117
    """

118 119
    # We don't commit the url object yet because we need to add its
    # owner, and parse its date field.
120
    url = url_form.save(commit=False)
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
    url.owner = request.user.registereduser

    # If the user entered a short url, it's already been validated,
    # so accept it. If they did not, however, then generate a
    # random one and use that instead.
    short = url_form.cleaned_data.get('short').strip()

    # Check if a short URL was entered
    if len(short) > 0:
        url.short = short
    else:
        # If the user didn't enter a short url, generate a random
        # one. However, if a random one can't be generated, return
        # a 500 server error.
        random_short = URL.generate_valid_short()

        if random_short is None:
            return 500
139 140

        url.short = random_short
141 142 143 144 145 146 147 148

    # Grab the expiration field value. It's currently an unsable
    # string value, so we need to parse it into a datetime object
    # relative to right now.
    expires = url_form.cleaned_data.get('expires')

    # Determine what the expiration date is
    if expires == URLForm.DAY:
149
        url.expires = timezone.now() + timedelta(days=1)
150
    elif expires == URLForm.WEEK:
151
        url.expires = timezone.now() + timedelta(weeks=1)
152
    elif expires == URLForm.MONTH:
153
        url.expires = timezone.now() + timedelta(weeks=3)
154 155
    # elif expires == URLForm.CUSTOM:
    #     url.expires = url_form.cleaned_data.get('expires_custom')
156 157 158 159 160 161 162 163
    else:
        pass  # leave the field NULL

    # Make sure that our new URL object is clean, then save it and
    # let's redirect to view this baby.
    url.full_clean()
    url.save()
    return url
164

David Haynes's avatar
David Haynes committed
165
def view(request, short):
166
    """
167 168
    This view allows the user to "view details" about a URL. Note that they
    do not need to be logged in to view this information.
169
    """
170

David Haynes's avatar
David Haynes committed
171
    # Get the current domain info
172
    domain = "%ss://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
173

David Haynes's avatar
David Haynes committed
174
    # Get the URL that is being requested
175
    url = get_object_or_404(URL, short__iexact=short)
176

David Haynes's avatar
David Haynes committed
177
    # Render view.html passing the specified URL and Domain to the template
178
    return render(request, 'view.html', {
179
        'url': url,
180
        'domain': domain,
181
    })
182

David Haynes's avatar
David Haynes committed
183
@login_required
184
def edit(request, short):
185
    """
186 187 188
    This view allows a logged in user to edit the details of a Go link that they
    own. They can modify any value that they wish. If `short` is modified then
    we will need to create a new link and copy over stats from the previous.
189
    """
190

191
    # Do not allow unapproved users to edit links
192 193
    if request.user.registereduser.blocked:
        return render(request, 'banned.html')
194

195 196
    # Get the URL that is going to be edited
    url = get_object_or_404(URL, short__iexact=short)
David Haynes's avatar
David Haynes committed
197

198
    # If the RegisteredUser is the owner of the URL
199
    if url.owner != request.user.registereduser:
200 201
        # do not allow them to edit
        raise PermissionDenied()
202

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
    # If a POST request is received, then the user has submitted a form and it's
    # time to parse the form and edit that URL object
    if request.method == 'POST':
        return _edit_post(request, url)

    # Initial data set here
    if url.expires != None:
        # Initialize a URL form with an expire date
        url_form = EditForm(host=request.META.get('HTTP_HOST'), initial={
            'target': url.target,
            'short': url.short,
            'expires': 'Custom Date',
            'expires_custom': url.expires
        })  # unbound form
    else:
        # Initialize a URL form without an expire date
        url_form = EditForm(host=request.META.get('HTTP_HOST'), initial={
            'target': url.target,
            'short': url.short,
            'expires': 'Never',
        })  # unbound form

    # Render index.html passing the form to the template
    return render(request, 'core/edit_link.html', {
        'form': url_form
    })

def _edit_post(request, url):
    # Now we initialize the form again but this time we have the POST
    # request
    url_form = EditForm(request.POST, host=request.META.get('HTTP_HOST'))

    # Make a copy of the old URL
    copy = url
    # Remove the old one
    url.delete()

    # Django will check the form to make sure it's valid
    if not url_form.is_valid():
        # Render index.html passing the form to the template
        return render(request, 'core/edit_link.html', {
            'form': url_form
        })

    # If the short changed then we need to create a new object and
    # migrate some data over
    if url_form.cleaned_data.get('short').strip() != copy.short:
        # Parse the form and create a new URL object
        res = post(request, url_form)

        # If there is a 500 error returned, handle it
        if res == 500:
            return HttpResponseServerError(render(request, '500.html'))

        # Else we can procede with the editing process
        # Migrate clicks data
        res.clicks = copy.clicks
        res.qrclicks = copy.qrclicks
        res.socialclicks = copy.socialclicks

        # Save the new URL
        res.save()

        # Redirect to the shiny new *edited URL
        return redirect('view', res.short)

    # The short was not edited and thus, we can directly edit the url
    if url_form.cleaned_data.get('target').strip() != copy.target:
        copy.target = url_form.cleaned_data.get('target').strip()
        copy.save()

    # Grab the expiration field value. It's currently an unsable
    # string value, so we need to parse it into a datetime object
    # relative to right now.
    expires = url_form.cleaned_data.get('expires')

    # Determine what the expiration date is
    if expires == URLForm.DAY:
        edited_expires = timezone.now() + timedelta(days=1)
    elif expires == URLForm.WEEK:
        edited_expires = timezone.now() + timedelta(weeks=1)
    elif expires == URLForm.MONTH:
        edited_expires = timezone.now() + timedelta(weeks=3)
    elif expires == URLForm.CUSTOM:
        edited_expires = url_form.cleaned_data.get('expires_custom')
    else:
        pass  # leave the field NULL

    if edited_expires != copy.expires:
        copy.expires = edited_expires
        copy.save()

    # Redirect to the shiny new *edited URL
    return redirect('view', copy.short)
David Haynes's avatar
David Haynes committed
297

David Haynes's avatar
David Haynes committed
298 299
@login_required
def delete(request, short):
300
    """
David Haynes's avatar
David Haynes committed
301 302
    This view deletes a URL if you have the permission to. User must be
    logged in and registered, and must also be the owner of the URL.
303
    """
304

David Haynes's avatar
David Haynes committed
305
    # Get the URL that is going to be deleted
306
    url = get_object_or_404(URL, short__iexact=short)
David Haynes's avatar
David Haynes committed
307

308
    if url.owner != request.user.registereduser:
David Haynes's avatar
David Haynes committed
309
        # do not allow them to delete
310
        raise PermissionDenied()
311

312 313
    url.delete()
    return redirect('my_links')
Jean Michel Rouly's avatar
Jean Michel Rouly committed
314

David Haynes's avatar
David Haynes committed
315
def redirection(request, short):
316
    """
David Haynes's avatar
David Haynes committed
317
    This view redirects a user based on the short URL they requested.
318
    """
Jean Michel Rouly's avatar
Jean Michel Rouly committed
319

David Haynes's avatar
David Haynes committed
320 321
    # Get the current domain info
    domain = "%s://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
322

David Haynes's avatar
David Haynes committed
323
    # Get the URL object that relates to the requested Go link
324
    url = get_object_or_404(URL, short__iexact=short)
David Haynes's avatar
David Haynes committed
325 326
    # Increment our clicks by one
    url.clicks += 1
Eyad Hasan's avatar
Eyad Hasan committed
327 328 329 330 331
    # Get the URL short link
    doesExist = URL.objects.get(short__iexact=short)
    # Checks to see if the link exists, if not we 404 the user.
    if doesExist.target is None:
        return redirect('go/404.html')
David Haynes's avatar
David Haynes committed
332
    # If the user is trying to make a Go link to itself, we 404 them
333
    if url.target == domain + short:
334
        return redirect('404.html')
335

David Haynes's avatar
David Haynes committed
336
    # If the user is coming from a QR request then increment qrclicks
337 338 339
    if 'qr' in request.GET:
        url.qrclicks += 1

David Haynes's avatar
David Haynes committed
340
    # If the user is coming from a social media request then increment qrclicks
341 342 343
    if 'social' in request.GET:
        url.socialclicks += 1

David Haynes's avatar
David Haynes committed
344
    # Save our data and redirect the user towards thier destination
Jean Michel Rouly's avatar
Jean Michel Rouly committed
345
    url.save()
346
    return redirect(url.target)
347

348 349
def staff_member_required(view_func, redirect_field_name=REDIRECT_FIELD_NAME, login_url='/'):
    """
David Haynes's avatar
David Haynes committed
350 351
    Decorator function for views that checks that the user is logged in and is
    a staff member, displaying the login page if necessary.
352 353
    """

354 355
    return user_passes_test(
        lambda u: u.is_active and u.is_staff,
356 357
        login_url=login_url,
        redirect_field_name=redirect_field_name
358 359
    )(view_func)

David Haynes's avatar
David Haynes committed
360 361
@staff_member_required
def useradmin(request):
362
    """
David Haynes's avatar
David Haynes committed
363 364
    This view is a simplified admin panel, so that staff don't need to log in
    to approve links
365
    """
David Haynes's avatar
David Haynes committed
366 367

    # If we receive a POST request
368
    if request.POST:
369
        return _useradmin_post(request)
370

371
    # Get a list of all RegisteredUsers that need to be approved
372
    current_users = RegisteredUser.objects.filter(blocked=False)
Zosman's avatar
Zosman committed
373
    # Get a list of all RegisteredUsers that are blocked
374
    blocked_users = RegisteredUser.objects.filter(blocked=True)
375

David Haynes's avatar
David Haynes committed
376
    # Pass that list to the template
377
    return render(request, 'admin/useradmin.html', {
378 379
        'current_users': current_users,
        'blocked_users': blocked_users
380
    })
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430

def _useradmin_post(request):
    # Get a list of the potential victims (users)
    userlist = request.POST.getlist('username')

    # If we're blocking users
    if '_block' in request.POST:
        for name in userlist:
            to_block = RegisteredUser.objects.get(user__username__exact=name)
            if settings.EMAIL_HOST and settings.EMAIL_PORT:
                user_mail = to_block.user.username + settings.EMAIL_DOMAIN
                send_mail(
                    'Your Account has been Blocked!',
                    ######################
                    'Hey there %s,\n\n'
                    'The Go admins have reviewed your application and have '
                    'blocked you from using Go.\n\n'
                    'Please reach out to srct@gmu.edu to appeal '
                    'this decision.\n\n'
                    '- Go Admins'
                    % (str(to_block.full_name)),
                    ######################
                    settings.EMAIL_FROM,
                    [user_mail]
                )
            to_block.blocked = True
            to_block.save()

    # If we're un-blocking users
    elif '_unblock' in request.POST:
        for name in userlist:
            to_un_block = RegisteredUser.objects.get(user__username__exact=name)
            if settings.EMAIL_HOST and settings.EMAIL_PORT:
                user_mail = to_un_block.user.username + settings.EMAIL_DOMAIN
                send_mail(
                    'Your Account has been Un-Blocked!',
                    ######################
                    'Hey there %s,\n\n'
                    'The Go admins have reviewed your application and have '
                    'Un-Blocked you from using Go.\n\n'
                    'Congratulations! '
                    '- Go Admins'
                    % (str(to_un_block.full_name)),
                    ######################
                    settings.EMAIL_FROM,
                    [user_mail]
                )
            to_un_block.blocked = False
            to_un_block.save()
            return HttpResponseRedirect('useradmin')