ethertoff
clone your own copy | download snapshot

Snapshots | iceberg

Inside this repository

views.py
text/x-python

Download raw (15.6 KB)

# -*- coding: utf-8 -*-

# Python imports
import datetime
import time
import urllib
from urllib.parse import urlparse
from html.parser import HTMLParser
import json
import re
import os

# PyPi imports

import markdown
from markdown.extensions.toc import TocExtension
from mdx_figcaption import FigcaptionExtension
from py_etherpad import EtherpadLiteClient
import dateutil.parser
import pytz

# Framework imports
from django.shortcuts import render, get_object_or_404

from django.http import HttpResponse, HttpResponseRedirect
from django.template import RequestContext
from django.template.defaultfilters import slugify
from django.urls import reverse
from django.template.context_processors import csrf
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import login_required
from django.utils.translation import ugettext_lazy as _
from django.db import IntegrityError

# Django Apps import

from etherpadlite.models import *
from etherpadlite import forms
from etherpadlite import config
from django.contrib.sites.shortcuts import get_current_site

from ethertoff.management.commands.index import snif

# By default, the homepage is the pad called ‘start’ (props to DokuWiki!)
try:
    from ethertoff.settings import HOME_PAD
except ImportError:
    HOME_PAD = 'About.md'
try:
    from ethertoff.settings import BACKUP_DIR
except ImportError:
    BACKUP_DIR = None

"""
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/
"""

h = HTMLParser()
unescape = h.unescape

allowed_extensions = ['.md', '.html', '.css']
default_extension = '.md'

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

def savePad(pad, slug, n=0):
    if n < 25:
        try:
            if n > 0:
                base, ext = os.path.splitext(slug)
                pad.display_slug = '{base}-{n}{ext}'.format(base=base, ext=ext, n=n)
                pad.name=slugify(pad.display_slug)[:42]
            pad.save()
        except IntegrityError:
            savePad(pad, slug, n+1)
        return pad
    return False

def createPad (slug, server, group, n=0):
    if n < 25:
        try:
            if n > 0:
                base, ext = os.path.splitext(slug)
                safe_slug = '{base}-{n}{ext}'.format(base=base, ext=ext, n=n)
            else:
                safe_slug = slug

            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
                display_name=safe_slug,     # this is just for backwards compatibility
                server=group.server,
                group=group
            )

            pad.save()
            return pad
        except IntegrityError:
            return createPad(slug=slug, server=server, group=group, n=n+1)

    return False

@login_required(login_url='/accounts/login')
def padCreate(request):
    """
    Create a pad
    """    
    
    # 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]
    
    if request.method == 'POST':  # Process the form
        form = forms.PadCreate(request.POST)
        if form.is_valid():
            n, ext = os.path.splitext(form.cleaned_data['name'])
            n = re.sub(r'\s+', '_', n)

            if ext in allowed_extensions:
                n = '{}{}'.format(n, ext)
            else:
                n = '{}{}'.format(n, default_extension)

            pad = createPad(slug=re.sub(r'[^\w\-_:\.]+', '-', n), server=group.server, group=group)

            return HttpResponseRedirect(reverse('pad-write', args=(pad.display_slug,) ))
    else:  # No form to process so create a fresh one
        form = forms.PadCreate({'group': group.groupID})

    con = {
        'form': form,
        'pk': group.pk,
        'title': _('Create pad in %(grp)s') % {'grp': group}
    }
    con.update(csrf(request))
    return render(
        request,
        'pad-create.html',
        con,
    )


@login_required(login_url='/accounts/login')
def pad(request, pk=None, slug=None): # pad_write
    """
    Create and session and display an embedded pad
    """

    # Initialize some needed values
    if slug:
        pad = get_object_or_404(Pad, display_slug=slug)
    else:
        pad = get_object_or_404(Pad, pk=pk)
    padLink = pad.server.url + '/p/' + pad.group.groupID + '$' + \
        urllib.parse.quote(pad.name)
    server = urlparse(pad.server.url)
    author = PadAuthor.objects.get(user=request.user)

    if author not in pad.group.authors.all():
        response = render(
            request,
            'pad.html',
            {
                'pad': pad,
                'link': padLink,
                'server': server,
                'uname': "{}".format(author.user),
                'error': _('You are not allowed to view or edit this pad')
            },
            context_instance=RequestContext(request)
        )
        return response

    # Create the session on the etherpad-lite side
    expires = datetime.datetime.utcnow() + datetime.timedelta(
        seconds=config.SESSION_LENGTH
    )
    epclient = EtherpadLiteClient(pad.server.apikey, pad.server.apiurl)

    # Try to use existing session as to allow editing multiple pads at once
    newSessionID = False

    try:
        if not 'sessionID' in request.COOKIES:
            newSessionID = True

            result = epclient.createSession(
                pad.group.groupID,
                author.authorID,
                time.mktime(expires.timetuple()).__str__()
            )
    except Exception as e:
        response =  render(
            request,
            'pad.html',
            {
                'pad': pad,
                'link': padLink,
                'server': server,
                'uname': "{}".format(author.user),
                'error': _('etherpad-lite session request returned:') +
                ' "' + e.reason if isinstance(e, UnicodeError) else str(e) + '"'
            }
        )
        return response

    # Set up the response
    response = render(
        request,
        'pad.html',
        {
            'pad': pad,
            'link': padLink,
            'server': server,
            'uname': "{}".format(author.user),
            'error': False,
            'mode' : 'write'
        },
    )

    if newSessionID:
        # 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
        )
        
    return response

def xhtml(request, slug):
    return pad_read(request, "r", slug + '.md')

def pad_read(request, mode="r", slug=None):
    """Read only pad
    """
    
    # FIND OUT WHERE WE ARE,
    # then get previous and next
    try:
        articles = json.load(open(os.path.join(BACKUP_DIR, 'index.json')))
    except IOError:
        articles = []
    
    SITE = get_current_site(request)
    href = "http://%s" % SITE.domain + request.path
    
    prev = None
    next = None
    for i, article in enumerate(articles):
        if article['href'] == href:
            if i != 0:        # The first is the most recent article, there is no newer
                next = articles[i-1]
            if i != len(articles) - 1:
                prev = articles[i+1]

    # Initialize some needed values
    pad = get_object_or_404(Pad, display_slug=slug)

    padID = pad.group.groupID + '$' + urllib.parse.quote(pad.name.replace('::', '_'))
    epclient = EtherpadLiteClient(pad.server.apikey, pad.server.apiurl)

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

    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)

    name, extension = os.path.splitext(slug)

    meta = {}

    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']:
            md = markdown.Markdown(extensions=['extra', 'meta', TocExtension(baselevel=2), 'attr_list', FigcaptionExtension()])
            text = md.convert(text)
            try:
                meta = md.Meta
            except AttributeError:   # Edge-case: this happens when the pad is completely empty
                meta = None
    
    # 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):
        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))
    
    text = include_regex.sub(ret, text)
    
    
    # Create namespaces from the url of the pad
    # 'pedagogy::methodology' -> ['pedagogy', 'methodology']
    namespaces = [p.rstrip('-') for p in pad.display_slug.split('::')]

    meta_list = []

    # 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']:
        if not request.user.is_authenticated:
            pass #raise PermissionDenied
    
    if meta and len(meta.keys()) > 0:
        
        # The human-readable date is parsed so we can sort all the articles
        if 'date' in meta:
            meta['date_iso'] = []
            meta['date_parsed'] = []
            for date in meta['date']:
                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() )

        meta_list = list(meta.items())

        print(meta_list)

    tpl_params = { 'pad'                : pad,
                   'meta'               : meta,      # to access by hash, like meta.author
                   'meta_list'          : meta_list, # to access all meta info in a (key, value) list
                   'text'               : text,
                   'prev_page'          : prev,
                   'next_page'          : next,
                   'mode'               : mode,
                   'namespaces'         : namespaces,
                   'authorship_authors_json' : authorship_authors_json,
                   'authors'            : authors }

    if not request.user.is_authenticated:
        request.session.set_test_cookie()
        tpl_params['next'] = reverse('pad-write', args=(slug,) )

    if mode == "r":
        return render(request, "pad-read.html", tpl_params)
    elif mode == "s":
        return render(request, "pad-slide.html", tpl_params)
    elif mode == "p":
        return render(request, "pad-print.html", tpl_params)



def home(request):
    try:
        articles = json.load(open(os.path.join(BACKUP_DIR, 'index.json')))
    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'))
    
    sort = 'date'
    if 'sort' in request.GET:
        sort = request.GET['sort']

    hash = {}
    for article in articles:
        if sort in article:
            if isinstance(article[sort], str):
                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)
        })

    tpl_params = { 'articles': tpl_articles,
                   'sort': sort }
    return render(request, "home.html", tpl_params)

@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'] = ""
    return render(request, "publish.html", tpl_params)

@login_required(login_url='/accounts/login')
def all(request):
    return render(request, "all.html")


def rss(request):
    from ethertoff.settings import SITE_DOMAIN
    context = {
        'SITE_DOMAIN': SITE_DOMAIN,
    }

    return render(request, "rss.xml", context, "text/xml")

def padOrFallbackPath(request, slug, fallbackPath, mimeType):
    try:
        pad = Pad.objects.get(display_slug=slug)
        padID = pad.group.groupID + '$' + urllib.parse.quote(pad.name.replace('::', '_'))
        epclient = EtherpadLiteClient(pad.server.apikey, pad.server.apiurl)
        return HttpResponse(epclient.getText(padID)['text'], content_type=mimeType)
    except:
        # If there is no pad called "css", loads a default css file
        f = open(fallbackPath, 'r')
        contents = f.read()
        f.close()
        return HttpResponse(contents, content_type=mimeType)

def css(request):
    return padOrFallbackPath(request, 'screen.css', 'ethertoff/static/css/screen.css', 'text/css')

def cssprint(request):
    return padOrFallbackPath(request, 'laser.css', 'ethertoff/static/css/laser.css', 'text/css')

def offsetprint(request):
    return padOrFallbackPath(request, 'offset.css', 'ethertoff/static/css/offset.css', 'text/css')

def css_slide(request):
    return padOrFallbackPath(request, 'slidy.css', 'ethertoff/static/css/slidy.css', 'text/css')