views.py 25.2 KB
Newer Older
1
2
3
4
5
6
# -*- coding: utf-8 -*-

# Python imports
import datetime
import time
import urllib
7
8
from urllib.parse import urlparse
from html.parser import HTMLParser
9
import json
eric's avatar
eric committed
10
import re
11
12
13
14
import os

# PyPi imports

alexandre's avatar
alexandre committed
15
import markdown
gijs's avatar
Import    
gijs committed
16
from markdown.extensions.toc import TocExtension
alexandre's avatar
alexandre committed
17
from mdx_semanticdata import SemanticDataExtension
18
from py_etherpad import EtherpadLiteClient
19
import dateutil.parser
eric's avatar
eric committed
20
import pytz
21
22

# Framework imports
gijs's avatar
gijs committed
23
from django.shortcuts import render, get_object_or_404, redirect
24

25
from django.http import HttpResponse, HttpResponseRedirect
26
from django.template import RequestContext
27
from django.template.defaultfilters import slugify
28
29
from django.urls import reverse
from django.template.context_processors import csrf
30
31
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext_lazy as _
gijs's avatar
gijs committed
32
from django.db import IntegrityError
alexandre's avatar
alexandre committed
33
from django.conf import settings
alexandre's avatar
alexandre committed
34
from django.contrib.staticfiles import finders
35

36
# Django Apps import
37

38
from etherpadlite.models import Pad, PadAuthor
39
40
41
from etherpadlite import forms
from etherpadlite import config

42
from ethertoff.management.commands.index import snif
gijs's avatar
gijs committed
43
from ethertoff.templatetags.wikify import wikifyPath, ensureTrailingSlash
44

gijs's avatar
gijs committed
45
46
from generator.management.commands.generate import generate as generateStatic

gijs's avatar
gijs committed
47
48
from . import forms as ethertoffForms

alexandre's avatar
alexandre committed
49
50
51
52
53
from ethertoff.forms import RenameFolderForm
from django.views.generic.edit import FormView
from django.urls import reverse_lazy


54
55
# By default, the homepage is the pad called ‘start’ (props to DokuWiki!)
try:
alexandre's avatar
alexandre committed
56
    HOME_PAD = settings.HOME_PAD
57
except ImportError:
58
    HOME_PAD = 'About.md'
eric's avatar
eric committed
59
try:
alexandre's avatar
alexandre committed
60
    BACKUP_DIR = settings.BACKUP_DIR
eric's avatar
eric committed
61
62
except ImportError:
    BACKUP_DIR = None
63

eric's avatar
eric committed
64
65
66
67
68
69
"""
Set up an HTMLParser for the sole purpose of unescaping
Etherpad’s HTML entities.
cf http://fredericiana.com/2010/10/08/decoding-html-entities-to-text-in-python/
"""

70
h = HTMLParser()
eric's avatar
eric committed
71
72
unescape = h.unescape

73
74
75
76
77
"""
Create a regex for our include template tag
"""
include_regex = re.compile("{%\s?include\s?\"([\w._-]+)\"\s?%}")

gijs's avatar
gijs committed
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def makeLeaf ():
    return { 'folders': {}, 'pads': [] }

def insertAt (path=[], tree=[], pad=''):
    if len(path) > 1:
        key = path.pop(0)
        if not key in tree['folders']:
            tree['folders'][key] = makeLeaf()

        tree['folders'][key] = insertAt(path, tree['folders'][key], pad)
    else:
        tree['pads'].append(pad)

    return tree

def insertPad (pad, tree):
alexandre's avatar
alexandre committed
94
95
    if settings.PAD_NAMESPACE_SEPARATOR in pad.display_slug:
        path = pad.display_slug.split(settings.PAD_NAMESPACE_SEPARATOR)
gijs's avatar
gijs committed
96
97
98
99
    else:
        path = []
    
    return insertAt(path, tree, pad)
gijs's avatar
gijs committed
100
101
102
103

# Perhaps move to the model?
def makePadPublic (pad, n=0):
    if not pad.is_public:
alexandre's avatar
alexandre committed
104
        epclient = EtherpadLiteClient(pad.server.apikey, settings.API_LOCAL_URL if settings.API_LOCAL_URL else pad.server.apiurl)
gijs's avatar
gijs committed
105
106
107
108
109
110
111
112
113
114
115
116
        tail = '' if n == 0 else '-{}'.format(n)
        publicid = pad.name+tail

        try:
            res = epclient.sendClientsMessage(pad.padid, "Please continue editing in the publicversion of this pad")
            print(res)
            res = epclient.copyPad(pad.padid, publicid)
            pad.is_public = True
            pad.publicpadid = publicid
            pad.save()

            return pad
gijs's avatar
gijs committed
117
        
gijs's avatar
gijs committed
118
119
120
121
122
        except ValueError:
            return makePadPublic(pad, n+1)

def makePadPrivate(pad):
    if pad.is_public:
alexandre's avatar
alexandre committed
123
        epclient = EtherpadLiteClient(pad.server.apikey, settings.API_LOCAL_URL if settings.API_LOCAL_URL else pad.server.apiurl)
gijs's avatar
gijs committed
124
125
126
127
128
129
        res = epclient.movePad(pad.publicpadid, pad.padid, force=True)

        pad.is_public = False
        pad.publicpadid = ''
        pad.save()
        return pad
gijs's avatar
gijs committed
130

gijs's avatar
gijs committed
131
132
133
134
135
136
137
138
139
140
141
142
# Filter out forbidden
def filterPadSlug(slug):
    # Replace spaces by '_'
    slug = re.sub(r'\s', '_', slug)
    # Replace forbidden characters
    slug = re.sub(r'[/]', '', slug)

    return slug 

def ensurePadExtension(slug):
    name, ext = os.path.splitext(slug)

alexandre's avatar
alexandre committed
143
144
145
    if settings.PAD_FORCE_EXTENSION:
        if ext.lower() not in settings.PAD_ALLOWED_EXTENSIONS:
            ext = settings.PAD_DEFAULT_EXTENSION
gijs's avatar
gijs committed
146
147
148
149
150

        return '{}{}'.format(name, ext.lower())

    return '{}{}'.format(name, ext.lower()) 

gijs's avatar
gijs committed
151
# FIXME: better name
gijs's avatar
gijs committed
152
def numerizePadName (name, n=0):
gijs's avatar
gijs committed
153
154
155
156
157
    name, ext = os.path.splitext(name)

    if n > 0:
        name = '{}-{}'.format(name, n)

gijs's avatar
gijs committed
158
    return '{}{}'.format(name, ext)
gijs's avatar
gijs committed
159

gijs's avatar
gijs committed
160
161
def treatPadName(slug, n):
    return numerizePadName(ensurePadExtension(filterPadSlug(slug)), n)
gijs's avatar
gijs committed
162
163
164
165

def createPad (slug, server, group, n=0):
    if n < 25:
        try:
gijs's avatar
gijs committed
166
            safe_slug = treatPadName(slug, n)
gijs's avatar
gijs committed
167
168
169
170
171
172
173
174
175
            pad = Pad(
                name=slugify(safe_slug)[:42], # This is the slug internally used by etherpad
                display_slug=safe_slug, # This is the slug we get to change afterwards
                server=group.server,
                group=group
            )

            pad.save()
            return pad
gijs's avatar
gijs committed
176
177
178
        except ValueError:
            # Pad already exists on the server
            return createPad(slug=slug, server=server, group=group, n=n+1)
gijs's avatar
gijs committed
179
        except IntegrityError:
gijs's avatar
gijs committed
180
181
182
            # Pad existed in the database, but not on the server
            # delete the created pad
            pad.Destroy()
gijs's avatar
gijs committed
183
184
185
186
            return createPad(slug=slug, server=server, group=group, n=n+1)

    return False

gijs's avatar
gijs committed
187
188
# Move as a property to the model ?
def getFolderName (slug):
alexandre's avatar
alexandre committed
189
190
    if settings.PAD_NAMESPACE_SEPARATOR in slug:
        return slug.rsplit(settings.PAD_NAMESPACE_SEPARATOR, 1)[0]
gijs's avatar
gijs committed
191
192
193
    else:
        return None

eric's avatar
eric committed
194
@login_required(login_url='/accounts/login')
gijs's avatar
gijs committed
195
def padCreate(request, prefix=''):
196
    """
eric's avatar
eric committed
197
    Create a pad
198
    """    
eric's avatar
eric committed
199
200
201
202
203
    
    # normally the ‘pads’ context processor should have made sure that these objects exist:
    author = PadAuthor.objects.get(user=request.user)
    group = author.group.all()[0]
    
204
205
206
    if request.method == 'POST':  # Process the form
        form = forms.PadCreate(request.POST)
        if form.is_valid():
gijs's avatar
gijs committed
207
            slug = re.sub(r'\s+', '_', form.cleaned_data['name'])
208
            slug = slug.strip(":")  # avoids leading and trailing "::"
gijs's avatar
gijs committed
209
            pad = createPad(slug=slug, server=group.server, group=group)
gijs's avatar
gijs committed
210

eric's avatar
eric committed
211
            return HttpResponseRedirect(reverse('pad-write', args=(pad.display_slug,) ))
gijs's avatar
gijs committed
212
213
214
    else: 
        # No form to process so create a fresh one
        # prefix should contain the name of the folder
215
        form = forms.PadCreate({'group': group.groupID, 'name': wikifyPath(ensureTrailingSlash(prefix) if prefix else '')})
216
217
218

    con = {
        'form': form,
eric's avatar
eric committed
219
        'pk': group.pk,
220
        'title': _('Create pad in %(grp)s') % {'grp': group}
221
222
    }
    con.update(csrf(request))
223
224
    return render(
        request,
eric's avatar
eric committed
225
        'pad-create.html',
226
227
228
229
        con,
    )


gijs's avatar
gijs committed
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
@login_required(login_url='/etherpad')
def padDelete(request, pk):
    """Delete a given pad
    """
    pad = get_object_or_404(Pad, pk=pk)

    # Any form submissions will send us back to the profile
    if request.method == 'POST':
        if 'confirm' in request.POST:
            pad.delete()
        return HttpResponseRedirect('/manage/')

    con = {
        'action': reverse('pad-delete', kwargs={'pk': pk}),
        'question': _('Really delete the pad {}?'.format(str(pad))),
        'title': _('Deleting {}'.format(str(pad))),
gijs's avatar
gijs committed
246
        'label': _('Delete')
gijs's avatar
gijs committed
247
248
249
250
251
252
253
254
    }
    con.update(csrf(request))
    return render(
        request,
        'pads/confirm.html',
        con
    )

gijs's avatar
gijs committed
255
256
257
258

def renamePad(pad, slug, n=0):
    pad.display_slug = treatPadName(slug, n)
    
alexandre's avatar
alexandre committed
259
    while n < settings.MAX_PAD_SAVE_TRIES:
gijs's avatar
gijs committed
260
261
262
263
264
        try:
            return pad.save()
        except IntegrityError:
            return renamePad(pad, slug, n+1)

265

gijs's avatar
gijs committed
266
267
268
269
270
271
272
@login_required(login_url='/etherpad')
def padRename(request, pk):
    pad = get_object_or_404(Pad, pk=pk)

    if request.method == 'POST':
        form = ethertoffForms.PadRename(request.POST)
        if form.is_valid():
273
274
275
276
277
278
279
280
281
282
283
            slug = re.sub(r'\s+', '_', form.cleaned_data['name'])
            slug = slug.strip(":")  # avoids leading and trailing "::"
            renamePad(pad, slug)

            path = getFolderName(pad.display_slug)
            if path:
                path.replace(settings.PAD_NAMESPACE_SEPARATOR, '/')
                return redirect('manage', path=path)
            else:
                return redirect('manage')

gijs's avatar
gijs committed
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
    else:
        form = ethertoffForms.PadRename({
            'pk': pad.pk,
            'name': pad.display_slug
        })

    context = {
        'form': form,
        'pk': pad.pk,
        'name': pad.display_slug,
        'title': _('Rename pad {}').format(str(pad))
    }

    context.update(csrf(request))

    return render(
        request,
        'pad-rename.html',
        context
    )

alexandre's avatar
alexandre committed
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
class RenameFolderView(FormView):
    template_name = 'folder-rename.html'
    form_class = RenameFolderForm
    success_url = reverse_lazy('manage')

    def get_initial(self):
        """Return the initial data to use for forms on this view."""
        old_name = self.kwargs.get("prefix", "") 
        old_name = old_name.replace('/', settings.PAD_NAMESPACE_SEPARATOR)
        old_name = old_name.strip(":")  # avoids leading and trailing "::"
        self.initial.update({"old_name": old_name})
        self.initial.update({"new_name": old_name})
        return super().get_initial()


    def form_valid(self, form):
        old_name = form.cleaned_data.get('old_name')
        new_name = form.cleaned_data.get('new_name')
        for pad in Pad.objects.filter(display_slug__startswith=old_name):
            current_display_slug = pad.display_slug
            new_display_slug = new_name + current_display_slug[len(old_name):]
            pad.display_slug = new_display_slug
            pad.save()
        return super().form_valid(form)

gijs's avatar
gijs committed
330
331
332
@login_required(login_url='/etherpad')
def padPublic(request, pk):
    """Delete a given pad
gijs's avatar
gijs committed
333
    """
gijs's avatar
gijs committed
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
    pad = get_object_or_404(Pad, pk=pk)

    # Any form submissions will send us back to the profile
    if request.method == 'POST':
        if 'confirm' in request.POST:
            makePadPublic(pad)
        return HttpResponseRedirect('/manage/')

    con = {
        'action': reverse('pad-public', kwargs={'pk': pk}),
        'question': _('Really make {} public?'.format(str(pad))),
        'title': _('Making {} public'.format(str(pad))),
        'label': _('Make public')
    }
    con.update(csrf(request))
    return render(
        request,
        'pads/confirm.html',
        con
    )


@login_required(login_url='/etherpad')
def padPrivate(request, pk):
    """Delete a given pad
359
    """
gijs's avatar
gijs committed
360
    pad = get_object_or_404(Pad, pk=pk)
361

gijs's avatar
gijs committed
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
    # Any form submissions will send us back to the profile
    if request.method == 'POST':
        if 'confirm' in request.POST:
            makePadPrivate(pad)
        return HttpResponseRedirect('/manage/')

    con = {
        'action': reverse('pad-private', kwargs={'pk': pk}),
        'question': _('Really make {} private?'.format(str(pad))),
        'title': _('Making {} private'.format(str(pad))),
        'label': _('Make private')
    }
    con.update(csrf(request))
    return render(
        request,
        'pads/confirm.html',
        con
    )

381
def pad(request, pk=None, slug=None, mode=None):
382
    if slug:
383
        pad = get_object_or_404(Pad, display_slug=slug)
384
385
    else:
        pad = get_object_or_404(Pad, pk=pk)
gijs's avatar
gijs committed
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

    if pad.is_public:
        return pad_write_public(request, pad)
    else:
        return pad_write(request, pad)

def pad_write_public(request, pad): # pad_write
    padLink = pad.server.url + 'p/' + pad.publicpadid
    server = urlparse(pad.server.url)
    
    if request.user.is_authenticated:
        author = PadAuthor.objects.get(user=request.user)
        uname = str(author.user)
    else:
        uname = None

    # Set up the response
    return render(
        request,
        'pad-public.html',
        {
            'pad': pad,
            'link': padLink,
            'server': server,
            'error': False,
            'mode' : 'write-public',
            'uname': None
        },
    )

@login_required(login_url='/accounts/login')
def pad_write(request, pad):
    # pad_write
    
    """
     Create and session and display an embedded pad
    """
gijs's avatar
gijs committed
423
    padLink = pad.server.url + 'p/' + pad.group.groupID + '$' + \
424
        urllib.parse.quote(pad.name)
425
426
427
    server = urlparse(pad.server.url)
    author = PadAuthor.objects.get(user=request.user)

alexandre's avatar
alexandre committed
428
    path = pad.display_slug.split(settings.PAD_NAMESPACE_SEPARATOR)
gijs's avatar
gijs committed
429
430
    crumbs = [(path[i], path[:i+1]) for i in range(len(path))]

431
    if author not in pad.group.authors.all():
432
433
        response = render(
            request,
eric's avatar
eric committed
434
            'pad.html',
435
436
437
438
            {
                'pad': pad,
                'link': padLink,
                'server': server,
439
                'uname': "{}".format(author.user),
440
                'error': _('You are not allowed to view or edit this pad')
gijs's avatar
gijs committed
441
442
            },
            context_instance=RequestContext(request)
443
444
445
446
447
448
449
        )
        return response

    # Create the session on the etherpad-lite side
    expires = datetime.datetime.utcnow() + datetime.timedelta(
        seconds=config.SESSION_LENGTH
    )
alexandre's avatar
alexandre committed
450
    epclient = EtherpadLiteClient(pad.server.apikey, settings.API_LOCAL_URL if settings.API_LOCAL_URL else pad.server.apiurl)
451

gijs's avatar
gijs committed
452
    # Try to use existing session as to allow editing multiple pads at once
gijs's avatar
gijs committed
453
    makeNewSessionID = False
gijs's avatar
gijs committed
454

455
    try:
gijs's avatar
gijs committed
456
        if not 'sessionID' in request.COOKIES:
gijs's avatar
gijs committed
457
            makeNewSessionID = True
gijs's avatar
gijs committed
458
459
460
461
462
463

            result = epclient.createSession(
                pad.group.groupID,
                author.authorID,
                time.mktime(expires.timetuple()).__str__()
            )
464
    except Exception as e:
gijs's avatar
gijs committed
465
        response =  render(
466
            request,
eric's avatar
eric committed
467
            'pad.html',
468
469
470
471
            {
                'pad': pad,
                'link': padLink,
                'server': server,
472
                'uname': "{}".format(author.user),
473
                'error': _('etherpad-lite session request returned:') +
474
                ' "' + e.reason if isinstance(e, UnicodeError) else str(e) + '"'
475
            }
476
477
478
479
        )
        return response

    # Set up the response
480
481
    response = render(
        request,
eric's avatar
eric committed
482
        'pad.html',
483
484
485
486
        {
            'pad': pad,
            'link': padLink,
            'server': server,
487
            'uname': "{}".format(author.user),
488
            'error': False,
gijs's avatar
gijs committed
489
490
            'mode' : 'write',
            'crumbs': crumbs
gijs's avatar
gijs committed
491
        },
492
493
    )

gijs's avatar
gijs committed
494
    if makeNewSessionID:
gijs's avatar
gijs committed
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
        # Delete the existing session first
        if ('padSessionID' in request.COOKIES):
            if 'sessionID' in request.COOKIES.keys():
                try:
                    epclient.deleteSession(request.COOKIES['sessionID'])
                except ValueError:
                    response.delete_cookie('sessionID', server.hostname)
            response.delete_cookie('padSessionID')

        # Set the new session cookie for both the server and the local site
        response.set_cookie(
            'sessionID',
            value=result['sessionID'],
            expires=expires,
            domain=server.hostname,
            httponly=False
        )
        response.set_cookie(
            'padSessionID',
            value=result['sessionID'],
            expires=expires,
            httponly=False
        )
        
519
    return response
520

gijs's avatar
gijs committed
521
522
# @FIXME either implement or remove?
# Archiving command
523
524
def xhtml(request, slug):
    return pad_read(request, "r", slug + '.md')
525

526
def pad_read(request, mode="r", slug=None):
527
528
    """Read only pad
    """
529
530
531
    
    # FIND OUT WHERE WE ARE,
    # then get previous and next
eric's avatar
eric committed
532
533
534
535
536
    try:
        articles = json.load(open(os.path.join(BACKUP_DIR, 'index.json')))
    except IOError:
        articles = []
    
gijs's avatar
gijs committed
537
    # FIXME: construct url based on settings?
538
    # SITE = get_current_site(request)
gijs's avatar
gijs committed
539
    #href = "http://%s" % SITE.domain + request.path
540
    
gijs's avatar
gijs committed
541
542
    href = request.path

543
544
    prev = None
    next = None
545
546
    for i, article in enumerate(articles):
        if article['href'] == href:
547
            if i != 0:        # The first is the most recent article, there is no newer
548
                next = articles[i-1]
549
            if i != len(articles) - 1:
550
                prev = articles[i+1]
551
552

    # Initialize some needed values
553
    pad = get_object_or_404(Pad, display_slug=slug)
alexandre's avatar
alexandre committed
554

alexandre's avatar
alexandre committed
555
556
    padID = pad.publicpadid if pad.is_public else pad.group.groupID + '$' + urllib.parse.quote(pad.name.replace(settings.PAD_NAMESPACE_SEPARATOR, '_'))
    epclient = EtherpadLiteClient(pad.server.apikey, settings.API_LOCAL_URL if settings.API_LOCAL_URL else pad.server.apiurl)
alexandre's avatar
alexandre committed
557

558
559
560
561
    # Etherpad gives us authorIDs in the form ['a.5hBzfuNdqX6gQhgz', 'a.tLCCEnNVJ5aXkyVI']
    # We link them to the Django users DjangoEtherpadLite created for us
    authorIDs = epclient.listAuthorsOfPad(padID)['authorIDs']
    authors = PadAuthor.objects.filter(authorID__in=authorIDs)
alexandre's avatar
alexandre committed
562

563
564
565
566
567
    authorship_authors = []
    for author in authors:
        authorship_authors.append({ 'name'  : author.user.first_name if author.user.first_name else author.user.username,
                                    'class' : 'author' + author.authorID.replace('.','_') })
    authorship_authors_json = json.dumps(authorship_authors, indent=2)
alexandre's avatar
alexandre committed
568

569
    name, extension = os.path.splitext(slug)
alexandre's avatar
alexandre committed
570
571
572

    meta = {}

573
574
575
576
577
578
579
580
581
582
583
    if not extension:
        # Etherpad has a quasi-WYSIWYG functionality.
        # Though is not alwasy dependable
        text = epclient.getHtml(padID)['html']
        # Quick and dirty hack to allow HTML in pads
        text = unescape(text)
    else:
        # If a pad is named something.css, something.html, something.md etcetera,
        # we don’t want Etherpads automatically generated HTML, we want plain text.
        text = epclient.getText(padID)['text']
        if extension in ['.md', '.markdown']:
alexandre's avatar
alexandre committed
584
            md = markdown.Markdown(extensions=['extra', 'meta', SemanticDataExtension({}), TocExtension(baselevel=2), 'attr_list'])
gijs's avatar
...    
gijs committed
585
            text = md.convert(text)
586
587
588
589
            try:
                meta = md.Meta
            except AttributeError:   # Edge-case: this happens when the pad is completely empty
                meta = None
590
591
592
593
    
    # Convert the {% include %} tags into a form easily digestible by jquery
    # {% include "example.html" %} -> <a id="include-example.html" class="include" href="/r/include-example.html">include-example.html</a>
    def ret(matchobj):
594
        return '<a id="include-%s" class="include pad-%s" href="%s">%s</a>' % (slugify(matchobj.group(1)), slugify(matchobj.group(1)), reverse('pad-read', args=("r", matchobj.group(1)) ), matchobj.group(1))
595
596
597
598
    
    text = include_regex.sub(ret, text)
    
    
599
    # Create namespaces from the url of the pad
600
    # 'pedagogy::methodology' -> ['pedagogy', 'methodology']
alexandre's avatar
alexandre committed
601
    namespaces = [p.rstrip('-') for p in pad.display_slug.split(settings.PAD_NAMESPACE_SEPARATOR)]
alexandre's avatar
alexandre committed
602

603
    meta_list = []
604

605
606
    # One needs to set the ‘Static’ metadata to ‘Public’ for the page to be accessible to outside visitors
    if not meta or not 'status' in meta or not meta['status'][0] or not meta['status'][0].lower() in ['public']:
607
        if not request.user.is_authenticated:
eric's avatar
eric committed
608
            pass #raise PermissionDenied
eric's avatar
eric committed
609
610
    
    if meta and len(meta.keys()) > 0:
611
612
        
        # The human-readable date is parsed so we can sort all the articles
613
        if 'date' in meta:
eric's avatar
eric committed
614
            meta['date_iso'] = []
615
616
            meta['date_parsed'] = []
            for date in meta['date']:
617
618
619
620
621
622
623
624
625
                try:
                    date_parsed = dateutil.parser.parse(date)
                    # If there is no timezone we assume it is in Brussels:
                    if not date_parsed.tzinfo:
                        date_parsed = pytz.timezone('Europe/Brussels').localize(date_parsed)
                    meta['date_parsed'].append(date_parsed)
                    meta['date_iso'].append( date_parsed.isoformat() )
                except ValueError:
                    continue
626
627
628

        meta_list = list(meta.items())

alexandre's avatar
alexandre committed
629

630
    tpl_params = { 'pad'                : pad,
eric's avatar
eric committed
631
632
                   'meta'               : meta,      # to access by hash, like meta.author
                   'meta_list'          : meta_list, # to access all meta info in a (key, value) list
633
                   'text'               : text,
634
635
                   'prev_page'          : prev,
                   'next_page'          : next,
636
                   'mode'               : mode,
637
638
639
                   'namespaces'         : namespaces,
                   'authorship_authors_json' : authorship_authors_json,
                   'authors'            : authors }
alexandre's avatar
alexandre committed
640

641
    if not request.user.is_authenticated:
642
643
        request.session.set_test_cookie()
        tpl_params['next'] = reverse('pad-write', args=(slug,) )
alexandre's avatar
alexandre committed
644

645
    if mode == "r":
646
        return render(request, "pad-read.html", tpl_params)
647
    elif mode == "s":
648
        return render(request, "pad-slide.html", tpl_params)
649
    elif mode == "p":
650
        return render(request, "pad-print.html", tpl_params)
651
652


653

654
def home(request):
655
656
    return all(request)
    
657
    try:
eric's avatar
eric committed
658
        articles = json.load(open(os.path.join(BACKUP_DIR, 'index.json')))
eric's avatar
eric committed
659
660
661
662
663
664
665
    except IOError: # If there is no index.json generated, we go to the defined homepage
        try:
            Pad.objects.get(display_slug=HOME_PAD)
            return pad_read(request, slug=HOME_PAD)
        except Pad.DoesNotExist: # If there is no homepage defined we go to the login:
            return HttpResponseRedirect(reverse('login'))
    
666
    sort = 'date'
eric's avatar
eric committed
667
668
    if 'sort' in request.GET:
        sort = request.GET['sort']
669
670
671
672

    hash = {}
    for article in articles:
        if sort in article:
673
            if isinstance(article[sort], str):
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
                subject = article[sort]
                if not subject in hash:
                    hash[subject] = [article]
                else:
                    hash[subject].append(article)
            else:
                for subject in article[sort]:
                    if not subject in hash:
                        hash[subject] = [article]
                    else:
                        hash[subject].append(article)
    tpl_articles = []
    for subject in sorted(hash.keys()):
        # Add the articles sorted by date ascending:
        tpl_articles.append({
            'key' : subject,
            'values': sorted(hash[subject], key=lambda a: a['date'] if 'date' in a else 0)
        })

eric's avatar
eric committed
693
694
    tpl_params = { 'articles': tpl_articles,
                   'sort': sort }
695
    return render(request, "home.html", tpl_params)
eric's avatar
eric committed
696

697
698
699
700
701
702
703
704
705
@login_required(login_url='/accounts/login')
def publish(request):
    tpl_params = {}
    if request.method == 'POST':
        tpl_params['published'] = True
        tpl_params['message'] = snif()
    else:
        tpl_params['published'] = False
        tpl_params['message'] = ""
706
    return render(request, "publish.html", tpl_params)
707

gijs's avatar
gijs committed
708
709
710
711
712
713
714
715
716
717
718
719
@login_required(login_url='/accounts/login')
def generate(request):
    tpl_params = {}
    if request.method == 'POST':
        tpl_params['generated'] = True
        tpl_params['message'] = generateStatic()
    else:
        tpl_params['generate'] = False
        tpl_params['message'] = ""
    return render(request, "generate.html", tpl_params)


gijs's avatar
gijs committed
720
721
722
723
724
@login_required(login_url='/accounts/login')
# def manage(request, page=1):
def manage(request, path=[]):
    if len(path) > 0:
        path = path.split('/')
alexandre's avatar
alexandre committed
725
        pads = Pad.objects.filter(display_slug__startswith=settings.PAD_NAMESPACE_SEPARATOR.join(path) + settings.PAD_NAMESPACE_SEPARATOR).order_by('name')
gijs's avatar
gijs committed
726
    else:
gijs's avatar
gijs committed
727
        pads = Pad.objects.all().order_by('name')
alexandre's avatar
alexandre committed
728
    # paginator = Paginator(pads, settings.PADS_PER_PAGE)
gijs's avatar
gijs committed
729
730
731
732
733
734
735
736
737
738

    tree = makeLeaf()

    for pad in pads:
        tree = insertPad(pad, tree)
    
    if len(path) > 0:
        for key in path:
            tree = tree['folders'][key]

gijs's avatar
gijs committed
739
740
741
742
    crumbs = [(path[i], path[:i+1]) for i in range(len(path))]

    folders = [key for key in tree['folders'].keys()]

gijs's avatar
gijs committed
743
    folders.sort(key=str.lower)
gijs's avatar
gijs committed
744

745
746
747
748
749
750
751
752
    return render(request, "manage-tree.html", {
        'tree': tree,
        'folderPath': path,
        'crumbs': crumbs,
        'folders': folders,
        'folderPathString': '/'.join(path) if path else None,
        'PAD_OPEN_MODE': settings.TREE_PAD_OPEN_MODE
    })
gijs's avatar
gijs committed
753
    
754
def all(request):
755
756
757
758
759
760
761
762
    if request.user.is_authenticated:
        return manage(request)
    else:
        return all_public(request)

def all_public(request):
    publicpads = Pad.objects.filter(is_public=True)
    return render(request, "all-public.html", { 'publicpads': publicpads })
763

764
765
766
@login_required(login_url='/accounts/login')
def all_private(request):
    return render(request, "all.html")
gijs's avatar
gijs committed
767

768
def padOrFallbackPath(request, slug, fallbackPath, mimeType):
769
    try:
770
        pad = Pad.objects.get(display_slug=slug)
alexandre's avatar
alexandre committed
771
772
        padID = pad.group.groupID + '$' + urllib.parse.quote(pad.name.replace(settings.PAD_NAMESPACE_SEPARATOR, '_'))
        epclient = EtherpadLiteClient(pad.server.apikey, settings.API_LOCAL_URL if settings.API_LOCAL_URL else pad.server.apiurl)
773
        return HttpResponse(epclient.getText(padID)['text'], content_type=mimeType)
774
    except:
775
        # If there is no pad called "css", loads a default css file
alexandre's avatar
alexandre committed
776
777
        path = finders.find(fallbackPath)
        f = open(path, 'r')
778
        contents = f.read()
779
        f.close()
780
781
782
        return HttpResponse(contents, content_type=mimeType)

def css(request):
alexandre's avatar
alexandre committed
783
    return padOrFallbackPath(request, 'screen.css', 'css/screen.css', 'text/css')
784

svilayphiou's avatar
svilayphiou committed
785
def cssprint(request):
alexandre's avatar
alexandre committed
786
    return padOrFallbackPath(request, 'laser.css', 'css/laser.css', 'text/css')
svilayphiou's avatar
merge    
svilayphiou committed
787
788

def offsetprint(request):
alexandre's avatar
alexandre committed
789
    return padOrFallbackPath(request, 'offset.css', 'css/offset.css', 'text/css')
790
791

def css_slide(request):
alexandre's avatar
alexandre committed
792
    return padOrFallbackPath(request, 'slidy.css', 'css/slidy.css', 'text/css')