<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Copyright 2014, Cercle Informatique ASBL. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This software was made by hast, C4, ititou at UrLab, ULB's hackerspace

from __future__ import absolute_import
from __future__ import unicode_literals

from celery import shared_task, chain
from os import path
import subprocess
import hashlib
import uuid
join = path.join
import tempfile
from wand.image import Image, Color

from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile

from documents.models import Document, Page
from notify.models import Notification
from .exceptions import DocumentProcessingError, MissingBinary

class SkipException(StandardError):
    pass

class ExisingChecksum(SkipException):
    pass

def on_failure(self, exc, task_id, args, kwargs, einfo):
    if isinstance(exc, SkipException):
        return None
    did = args[0]
    print("Document {} failed.".format(did))

    document = Document.objects.get(id=did)
    document.state = "ERROR"
    document.save()

    Notification.direct(
        user=document.user,
        text="Une erreur c'est produite pendant la conversion de : {}".format(document.name),
        node=document.parent,
        url=reverse('node_canonic', args=[document.parent.id]),
        icon="x",
    )
    # TODO : alert admins

def doctask(*args, **kwargs):
    return shared_task(*args, bind=True, on_failure=on_failure, **kwargs)

@doctask
def process_document(self, document_id):
    document = Document.objects.get(pk=document_id)

    if document.state == "IN_QUEUE":
        document.state = "PROCESSING"
        document.save()
    elif document.state in ("PREPARING", "PROCESSING", "READY_TO_QUEUE"):
        raise DocumentProcessingError(document_id, "Wrong state : {}".format(document.state))
    elif document.state in ("DONE", "ERROR"):
        raise NotImplementedError(document_id, document.state)
        # TODO : clean destination + celery.send_task()

    if document.file_type in ('.pdf', 'application/pdf'):
        document.pdf = document.original
        document.save()
        process_pdf.delay(document_id)
    else:
        process_office.delay(document_id)

@doctask
def sanity_check(self, document_id):
    document = Document.objects.get(pk=document_id)

    if not document.original:
        raise DocumentProcessingError(document_id, "Missing original")

    return document_id

@doctask
def checksum(self, document_id):
    document = Document.objects.get(pk=document_id)

    contents = document.original.read()

    hashed = hashlib.md5(contents).hexdigest()
    query = Document.objects.filter(md5=hashed).exclude(md5='')
    if query.count() != 0:
        dup = query.first()
        Notification.direct(
            user=document.user,
            text='Votre document "{}" a été refusé car c\'est une copie conforme de {}'.format(document.name, dup.name),
            node=document.parent,
            url=reverse('node_canonic', args=[dup.id]),
            icon="x",
        )
        did = document.id
        document.delete()
        raise ExisingChecksum("Document {} has the same checksum as {}".format(did, dup.id))
    else:
        document.md5 = hashed
        document.save()

    return document_id

checksum.throws = (ExisingChecksum,)

@doctask
def convert_office_to_pdf(self, document_id):
    document = Document.objects.get(pk=document_id)

    tmp = tempfile.NamedTemporaryFile()
    tmp.write(document.original.read())
    tmp.flush()

    try:
        sub = subprocess.check_output(['unoconv', '-f', 'pdf', '--stdout', tmp.name])
    except OSError:
        raise MissingBinary("unoconv")
    except subprocess.CalledProcessError as e:
        raise DocumentProcessingError(document, exc=e, message='"unoconv" has failed')

    document.pdf.save(str(uuid.uuid4()), ContentFile(sub))

    tmp.close()

    return document_id

@doctask
def preview_pdf(self, document_id):

    document = Document.objects.get(pk=document_id)

    pdf = Image(file=document.pdf)

    document.pages = len(pdf.sequence)
    document.save()

    jpg_document = pdf.convert('jpg')
    pdf.close()

    for i in range(document.pages):
        page = Page.objects.create(numero=i)
        document.add_child(page, acyclic_check=False)

        jpg_page = jpg_document.sequence[i]
        jpg_page.background = Color('#ffffff')

        for width in 120, 600, 900:
            cloned = Image(jpg_page)
            cloned.compression_quality = 90
            cloned.transform(resize=str(width))
            result = cloned.make_blob()
            cloned.close()
            destination = page.__getattribute__('bitmap_' + str(width))
            destination.save(str(uuid.uuid4()), ContentFile(result))

    jpg_document.close()

    return document_id

@doctask
def finish_file(self, document_id):
    document = Document.objects.get(pk=document_id)
    document.state = 'DONE'
    document.save()

    return document_id

process_pdf = chain(
    sanity_check.s(),
    checksum.s(),
    preview_pdf.s(),
    finish_file.s()
)

process_office = chain(
    sanity_check.s(),
    checksum.s(),
    convert_office_to_pdf.s(),
    preview_pdf.s(),
    finish_file.s()
)
<commit_msg>Remove the alpha layer to fix black thunmbnails and trasparents bg<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Copyright 2014, Cercle Informatique ASBL. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This software was made by hast, C4, ititou at UrLab, ULB's hackerspace

from __future__ import absolute_import
from __future__ import unicode_literals

from celery import shared_task, chain
from os import path
import subprocess
import hashlib
import uuid
join = path.join
import tempfile
from wand.image import Image, Color

from django.core.urlresolvers import reverse
from django.core.files.base import ContentFile

from documents.models import Document, Page
from notify.models import Notification
from .exceptions import DocumentProcessingError, MissingBinary

class SkipException(StandardError):
    pass

class ExisingChecksum(SkipException):
    pass

def on_failure(self, exc, task_id, args, kwargs, einfo):
    if isinstance(exc, SkipException):
        return None
    did = args[0]
    print("Document {} failed.".format(did))

    document = Document.objects.get(id=did)
    document.state = "ERROR"
    document.save()

    Notification.direct(
        user=document.user,
        text="Une erreur c'est produite pendant la conversion de : {}".format(document.name),
        node=document.parent,
        url=reverse('node_canonic', args=[document.parent.id]),
        icon="x",
    )
    # TODO : alert admins

def doctask(*args, **kwargs):
    return shared_task(*args, bind=True, on_failure=on_failure, **kwargs)

@doctask
def process_document(self, document_id):
    document = Document.objects.get(pk=document_id)

    if document.state == "IN_QUEUE":
        document.state = "PROCESSING"
        document.save()
    elif document.state in ("PREPARING", "PROCESSING", "READY_TO_QUEUE"):
        raise DocumentProcessingError(document_id, "Wrong state : {}".format(document.state))
    elif document.state in ("DONE", "ERROR"):
        raise NotImplementedError(document_id, document.state)
        # TODO : clean destination + celery.send_task()

    if document.file_type in ('.pdf', 'application/pdf'):
        document.pdf = document.original
        document.save()
        process_pdf.delay(document_id)
    else:
        process_office.delay(document_id)

@doctask
def sanity_check(self, document_id):
    document = Document.objects.get(pk=document_id)

    if not document.original:
        raise DocumentProcessingError(document_id, "Missing original")

    return document_id

@doctask
def checksum(self, document_id):
    document = Document.objects.get(pk=document_id)

    contents = document.original.read()

    hashed = hashlib.md5(contents).hexdigest()
    query = Document.objects.filter(md5=hashed).exclude(md5='')
    if query.count() != 0:
        dup = query.first()
        Notification.direct(
            user=document.user,
            text='Votre document "{}" a été refusé car c\'est une copie conforme de {}'.format(document.name, dup.name),
            node=document.parent,
            url=reverse('node_canonic', args=[dup.id]),
            icon="x",
        )
        did = document.id
        document.delete()
        raise ExisingChecksum("Document {} has the same checksum as {}".format(did, dup.id))
    else:
        document.md5 = hashed
        document.save()

    return document_id

checksum.throws = (ExisingChecksum,)

@doctask
def convert_office_to_pdf(self, document_id):
    document = Document.objects.get(pk=document_id)

    tmp = tempfile.NamedTemporaryFile()
    tmp.write(document.original.read())
    tmp.flush()

    try:
        sub = subprocess.check_output(['unoconv', '-f', 'pdf', '--stdout', tmp.name])
    except OSError:
        raise MissingBinary("unoconv")
    except subprocess.CalledProcessError as e:
        raise DocumentProcessingError(document, exc=e, message='"unoconv" has failed')

    document.pdf.save(str(uuid.uuid4()), ContentFile(sub))

    tmp.close()

    return document_id

@doctask
def preview_pdf(self, document_id):

    document = Document.objects.get(pk=document_id)

    pdf = Image(file=document.pdf, resolution=300)

    document.pages = len(pdf.sequence)
    document.save()

    jpg_document = pdf.convert('jpg')
    pdf.close()

    for i in range(document.pages):
        page = Page.objects.create(numero=i)
        document.add_child(page, acyclic_check=False)

        jpg_page = jpg_document.sequence[i]
        jpg_page.alpha_channel = False
        jpg_page.background = Color('#ffffff')

        for width in 120, 600, 900:
            cloned = Image(jpg_page)
            cloned.compression_quality = 90
            cloned.transform(resize=str(width))
            result = cloned.make_blob()
            cloned.close()
            destination = page.__getattribute__('bitmap_' + str(width))
            destination.save(str(uuid.uuid4()), ContentFile(result))

    jpg_document.close()

    return document_id

@doctask
def finish_file(self, document_id):
    document = Document.objects.get(pk=document_id)
    document.state = 'DONE'
    document.save()

    return document_id

process_pdf = chain(
    sanity_check.s(),
    checksum.s(),
    preview_pdf.s(),
    finish_file.s()
)

process_office = chain(
    sanity_check.s(),
    checksum.s(),
    convert_office_to_pdf.s(),
    preview_pdf.s(),
    finish_file.s()
)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
#
# Copyright 2015, Foxugly. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.

from django.http import HttpResponse
from django.shortcuts import render
from documents.models import Document, DocumentAdminForm, DocumentForm
from categories.models import Category
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
import json
import os
from subprocess import Popen
from django.core.exceptions import PermissionDenied
import logging
import shutil

logger = logging.getLogger(__name__)
logger.setLevel(settings.LOGGER)

def view(request, doc_id):
    logger.debug("viewy | doc_id = %d" % doc_id)
    d = get_object_or_404(Document, id=doc_id)
    if d.refer_category.refer_trimester.refer_year.refer_company in request.user.userprofile.companies.all().order_by('name'):
        img = ''
        for p in d.pages.all().order_by('num'):
            img += r"" % p.get_relative_path()
        c = dict(img=img)
        return render(request, 'doc.tpl', c)
    else:
        raise PermissionDenied

def document_view(request, doc_id):
    logger.debug("document_view | doc_id = %d" % doc_id)
    result = {}
    if request.is_ajax():
        d = get_object_or_404(Document, id=doc_id)
        result['name'] = d.name
        result['img'] = ''
        i = 1
        for p in d.pages.all().order_by('num'):
            result['img'] += r"" % p.get_relative_path()
            result['img'] += r"
%s %d
" % (_('Page'), i)
            i += 1
    return HttpResponse(json.dumps(result))

def update_ajax(request, doc_id):
    logger.debug('update_ajax | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, id=doc_id)
        if request.user.is_superuser:
            form = DocumentAdminForm(request.GET, instance=doc)
        else:
            form = DocumentForm(request.GET, instance=doc)
        results['n'] = doc.refer_category.count_docs()
        if form.is_valid():
            form.save()
            results['return'] = True
        else:
            results['return'] = False
            results['errors'] = str(form.errors)
    return HttpResponse(json.dumps(results))

def ajax_move(request, doc_id):
    logger.debug('ajax_move | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        cat = doc.refer_category
        if cat.count_docs() > 0:
            results['doc_id'] = doc_id
            tri = cat.refer_trimester
            year = tri.refer_year
            company = year.refer_company
            results['category'] = cat.as_json()
            results['trimester'] = tri.as_json()
            results['year'] = year.as_json()
            results['company'] = company.as_json()
            results['companies'] = [c.as_json() for c in request.user.userprofile.get_companies()]
            results['valid'] = True
        else:
            results['valid'] = False
    return HttpResponse(json.dumps(results))

def ajax_merge(request, doc_id):
    logger.debug('ajax_merge | doc_id = %d' % doc_id)
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        if doc.refer_category.count_docs() > 1:
            return HttpResponse(json.dumps([d.as_json() for d in doc.refer_category.documents.exclude(id=doc.id)]))
        else:
            return HttpResponse(json.dumps([]))

def ajax_split(request, doc_id):
    logger.debug('ajax_split | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        if doc.get_npages() > 1:
            results['doc_id'] = doc.id
            results['name'] = doc.name
            results['valid'] = True
            results['nname'] = "new doc"
            results['img'] = doc.pages.all()[0].as_img()
            results['size'] = doc.get_npages()
        else:
            results['valid'] = False
    return HttpResponse(json.dumps(results))

def move_document(doc_id, cat_id):
    logger.debug('move_document | doc_id = %d | cat_id = %d' % (doc_id, cat_id))
    doc = get_object_or_404(Document, pk=doc_id)
    old_cat = doc.refer_category
    new_cat = get_object_or_404(Category, pk=cat_id)
    for p in doc.pages.all():
        shutil.move(p.get_absolute_path(), new_cat.get_absolute_path())
    doc.refer_category = new_cat
    doc.save()
    old_cat.documents.remove(doc)
    new_cat.documents.add(doc)
    return True

def ajax_move_doc(request, doc_id, cat_id):
    logger.debug('ajax_move_doc | doc_id = %d | cat_id = %d' % (doc_id, cat_id))
    results = {}
    if request.is_ajax():
        results['valid'] = move_document(doc_id, cat_id)
    return HttpResponse(json.dumps(results))

def delete_document(doc_id):
    logger.debug('delete_document | doc_id = %d' % doc_id)
    doc = get_object_or_404(Document, pk=doc_id)
    doc.refer_category.documents.remove(doc)
    doc.delete()

def ajax_delete(request, doc_id):
    logger.debug('ajax_delete | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        results['valid'] = delete_document(doc_id)
    return HttpResponse(json.dumps(results))

def ajax_img(request, doc_id, num):
    logger.debug('ajax_img | doc_id = %d | num = %d' % (doc_id, num))
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        results['img'] = doc.pages.all()[int(num) - 1].as_img()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def split_doc(request):
    logger.debug('split_doc')
    results = {}
    if request.is_ajax():
        cut = int(request.GET['modal_split_cut'][0])
        doc = get_object_or_404(Document, pk=int(request.GET['modal_split_doc_id']))
        doc.name = request.GET['modal_split_name']
        new_doc = Document(name=request.GET['modal_split_new_name'], owner=request.user,
                           refer_category=doc.refer_category, complete=True)
        pages = doc.all_pages()
        new_doc.save()
        i = 0
        for p in pages:
            if i >= cut:
                new_doc.pages.add(p)
                new_doc.size += p.get_size()
                p.refer_document = new_doc
                p.save()
                doc.size -= p.get_size()
                doc.pages.remove(p)
            i += 1
        doc.refer_category.documents.add(new_doc)
        doc.save()
        new_doc.save()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def merge_doc(request):
    logger.debug('merge_doc')
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=int(request.GET['modal_merge_doc_id']))
        l = request.GET['doc_ids'].split(',')
        for add_doc in l:
            old_doc = get_object_or_404(Document, pk=int(add_doc))
            for p in old_doc.all_pages():
                doc.pages.add(p)
                p.refer_document = doc
                p.save()
                old_doc.pages.remove(p)
                old_doc.save()
                doc.size += p.get_size()
            old_doc.delete()
        doc.save()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_download(request, doc_id):
    logger.debug('ajax_download | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        # name = doc.name.replace(' ', '_')
        if ".pdf" != doc.name[-4:]:
            name += ".pdf"
        output_abs = settings.TMP_ROOT + name
        output_rel = settings.TMP_URL + name
        # TODO change ouvrir popup avec lien et utiliser le broker
        cmd = "convert "
        for p in doc.all_pages():
            cmd += p.get_absolute_path() + " "
        cmd += output_abs
        if os.path.exists(output_abs):
            os.system("rm " + output_abs)
        p1 = Popen(cmd, shell=True)
        p1.wait()
        sleep(1)
        results['url'] = output_rel
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_move(request, cat_id):
    logger.debug("ajax_multiple_move | cat_id = %d" % cat_id)
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            move_document(val[0], cat_id)
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_delete(request):
    logger.debug("ajax_multiple_delete")
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            delete_document(val[0])
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_download(request):
    logger.debug('ajax_multiple_download')
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            print('%s : %s' % (key, val))
        #TODO finish multiple download broker
        results['valid'] = True
    return HttpResponse(json.dumps(results))
<commit_msg>get_object_or_404 + logger<commit_after># -*- coding: utf-8 -*-
#
# Copyright 2015, Foxugly. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.

from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404
from documents.models import Document, DocumentAdminForm, DocumentForm
from categories.models import Category
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
import json
import os
from subprocess import Popen
from django.core.exceptions import PermissionDenied
import logging
import shutil

logger = logging.getLogger(__name__)
logger.setLevel(settings.LOGGER)

def view(request, doc_id):
    logger.debug("viewy | doc_id = %d" % doc_id)
    d = get_object_or_404(Document, id=doc_id)
    if d.refer_category.refer_trimester.refer_year.refer_company in request.user.userprofile.companies.all().order_by('name'):
        img = ''
        for p in d.pages.all().order_by('num'):
            img += r"" % p.get_relative_path()
        c = dict(img=img)
        return render(request, 'doc.tpl', c)
    else:
        raise PermissionDenied

def document_view(request, doc_id):
    logger.debug("document_view | doc_id = %d" % doc_id)
    result = {}
    if request.is_ajax():
        d = get_object_or_404(Document, id=doc_id)
        result['name'] = d.name
        result['img'] = ''
        i = 1
        for p in d.pages.all().order_by('num'):
            result['img'] += r"" % p.get_relative_path()
            result['img'] += r"
%s %d
" % (_('Page'), i)
            i += 1
    return HttpResponse(json.dumps(result))

def update_ajax(request, doc_id):
    logger.debug('update_ajax | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, id=doc_id)
        if request.user.is_superuser:
            form = DocumentAdminForm(request.GET, instance=doc)
        else:
            form = DocumentForm(request.GET, instance=doc)
        results['n'] = doc.refer_category.count_docs()
        if form.is_valid():
            form.save()
            results['return'] = True
        else:
            results['return'] = False
            results['errors'] = str(form.errors)
    return HttpResponse(json.dumps(results))

def ajax_move(request, doc_id):
    logger.debug('ajax_move | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        cat = doc.refer_category
        if cat.count_docs() > 0:
            results['doc_id'] = doc_id
            tri = cat.refer_trimester
            year = tri.refer_year
            company = year.refer_company
            results['category'] = cat.as_json()
            results['trimester'] = tri.as_json()
            results['year'] = year.as_json()
            results['company'] = company.as_json()
            results['companies'] = [c.as_json() for c in request.user.userprofile.get_companies()]
            results['valid'] = True
        else:
            results['valid'] = False
    return HttpResponse(json.dumps(results))

def ajax_merge(request, doc_id):
    logger.debug('ajax_merge | doc_id = %d' % doc_id)
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        if doc.refer_category.count_docs() > 1:
            return HttpResponse(json.dumps([d.as_json() for d in doc.refer_category.documents.exclude(id=doc.id)]))
        else:
            return HttpResponse(json.dumps([]))

def ajax_split(request, doc_id):
    logger.debug('ajax_split | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        if doc.get_npages() > 1:
            results['doc_id'] = doc.id
            results['name'] = doc.name
            results['valid'] = True
            results['nname'] = "new doc"
            results['img'] = doc.pages.all()[0].as_img()
            results['size'] = doc.get_npages()
        else:
            results['valid'] = False
    return HttpResponse(json.dumps(results))

def move_document(doc_id, cat_id):
    logger.debug('move_document | doc_id = %d | cat_id = %d' % (doc_id, cat_id))
    doc = get_object_or_404(Document, pk=doc_id)
    old_cat = doc.refer_category
    new_cat = get_object_or_404(Category, pk=cat_id)
    for p in doc.pages.all():
        shutil.move(p.get_absolute_path(), new_cat.get_absolute_path())
    doc.refer_category = new_cat
    doc.save()
    old_cat.documents.remove(doc)
    new_cat.documents.add(doc)
    return True

def ajax_move_doc(request, doc_id, cat_id):
    logger.debug('ajax_move_doc | doc_id = %d | cat_id = %d' % (doc_id, cat_id))
    results = {}
    if request.is_ajax():
        results['valid'] = move_document(doc_id, cat_id)
    return HttpResponse(json.dumps(results))

def delete_document(doc_id):
    logger.debug('delete_document | doc_id = %d' % doc_id)
    doc = get_object_or_404(Document, pk=doc_id)
    doc.refer_category.documents.remove(doc)
    doc.delete()

def ajax_delete(request, doc_id):
    logger.debug('ajax_delete | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        results['valid'] = delete_document(doc_id)
    return HttpResponse(json.dumps(results))

def ajax_img(request, doc_id, num):
    logger.debug('ajax_img | doc_id = %d | num = %d' % (doc_id, num))
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        results['img'] = doc.pages.all()[int(num) - 1].as_img()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def split_doc(request):
    logger.debug('split_doc')
    results = {}
    if request.is_ajax():
        cut = int(request.GET['modal_split_cut'][0])
        doc = get_object_or_404(Document, pk=int(request.GET['modal_split_doc_id']))
        doc.name = request.GET['modal_split_name']
        new_doc = Document(name=request.GET['modal_split_new_name'], owner=request.user,
                           refer_category=doc.refer_category, complete=True)
        pages = doc.all_pages()
        new_doc.save()
        i = 0
        for p in pages:
            if i >= cut:
                new_doc.pages.add(p)
                new_doc.size += p.get_size()
                p.refer_document = new_doc
                p.save()
                doc.size -= p.get_size()
                doc.pages.remove(p)
            i += 1
        doc.refer_category.documents.add(new_doc)
        doc.save()
        new_doc.save()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def merge_doc(request):
    logger.debug('merge_doc')
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=int(request.GET['modal_merge_doc_id']))
        l = request.GET['doc_ids'].split(',')
        for add_doc in l:
            old_doc = get_object_or_404(Document, pk=int(add_doc))
            for p in old_doc.all_pages():
                doc.pages.add(p)
                p.refer_document = doc
                p.save()
                old_doc.pages.remove(p)
                old_doc.save()
                doc.size += p.get_size()
            old_doc.delete()
        doc.save()
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_download(request, doc_id):
    logger.debug('ajax_download | doc_id = %d' % doc_id)
    results = {}
    if request.is_ajax():
        doc = get_object_or_404(Document, pk=doc_id)
        # name = doc.name.replace(' ', '_')
        if ".pdf" != doc.name[-4:]:
            name += ".pdf"
        output_abs = settings.TMP_ROOT + name
        output_rel = settings.TMP_URL + name
        # TODO change ouvrir popup avec lien et utiliser le broker
        cmd = "convert "
        for p in doc.all_pages():
            cmd += p.get_absolute_path() + " "
        cmd += output_abs
        if os.path.exists(output_abs):
            os.system("rm " + output_abs)
        p1 = Popen(cmd, shell=True)
        p1.wait()
        sleep(1)
        results['url'] = output_rel
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_move(request, cat_id):
    logger.debug("ajax_multiple_move | cat_id = %d" % cat_id)
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            move_document(val[0], cat_id)
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_delete(request):
    logger.debug("ajax_multiple_delete")
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            delete_document(val[0])
        results['valid'] = True
    return HttpResponse(json.dumps(results))

def ajax_multiple_download(request):
    logger.debug('ajax_multiple_download')
    results = {}
    if request.is_ajax():
        for key, val in dict(request.GET).items():
            print('%s : %s' % (key, val))
        #TODO finish multiple download broker
        results['valid'] = True
    return HttpResponse(json.dumps(results))
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Copyright 2014, Cercle Informatique ASBL. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This software was made by hast, C4, ititou at UrLab, ULB's hackerspace

import os
import uuid

from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.db.models import F

from documents.models import Document, Page
from graph.models import Course
from polydag.models import Node
from documents.forms import UploadFileForm, FileForm

from cycle import add_document_to_queue

@login_required
def upload_file(request, parent_id):
    parentNode = get_object_or_404(Node, id=parent_id)
    if not isinstance(parentNode, Course):
        raise NotImplementedError("Not a course")

    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)

        if form.is_valid():
            if len(form.cleaned_data['name']) > 0:
                name = form.cleaned_data['name']
            else:
                name, _ = os.path.splitext(request.FILES['file'].name)
                name = name.lower()

            extension = os.path.splitext(request.FILES['file'].name)[1].lower()
            description = form.cleaned_data['description']
            course = parentNode

            doc = Document.objects.create(
                user=request.user,
                name=name,
                description=description,
                state="PREPARING",
                file_type=extension
            )
            doc.original.save(str(uuid.uuid4()), request.FILES['file'])
            doc.save()

            course.add_child(doc)

            doc.add_keywords(*form.cleaned_data['tags'])
            doc.year = form.cleaned_data['year']

            doc.state = 'READY_TO_QUEUE'
            doc.save()

            add_document_to_queue(doc)

            return HttpResponseRedirect(reverse('course_show', args=[course.slug]))

    else:
        form = UploadFileForm()

    return render(request, 'document_upload.html', {
        'form': form,
        'parent': parentNode,
    })

@login_required
def document_edit(request, document_id):
    doc = get_object_or_404(Document, id=document_id)

    if request.user != doc.user and not request.user.is_moderator(doc.parent):
        return HttpResponse('
403
', status=403)

    if request.method == 'POST':
        form = FileForm(request.POST)

        if form.is_valid():
            doc.name = form.cleaned_data['name']
            doc.description = form.cleaned_data['description']

            doc.keywords.clear()
            doc.add_keywords(*form.cleaned_data['tags'])

            doc.year = form.cleaned_data['year']
            doc.save()

            return HttpResponseRedirect(reverse('document_show', args=[doc.id]))

    else:
        form = FileForm({
            'name': doc.name,
            'description': doc.description,
            'year': doc.year,
            'tags': doc.keywords.all()
        })

    return render(request, 'document_edit.html', {
        'form': form,
        'doc': doc,
    })

@login_required
def document_download(request, id):
    doc = get_object_or_404(Document, id=id)
    with open(doc.pdf) as fd:
        body = fd.read()
    response = HttpResponse(body, content_type='application/pdf')
    response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % (doc.name)
    doc.downloads += 1
    doc.save()
    return response

@login_required
def document_download_original(request, id):
    doc = get_object_or_404(Document, id=id)
    with open(doc.original) as fd:
        body = fd.read()
    response = HttpResponse(body, content_type='application/octet-stream')
    response['Content-Description'] = 'File Transfer'
    response['Content-Transfer-Encoding'] = 'binary'
    response['Content-Disposition'] = 'attachment; filename="{}{}"'.format(doc.name, doc.file_type)
    doc.downloads += 1
    doc.save()
    return response

@login_required
def document_show(request, id):
    document = get_object_or_404(Document, id=id)

    children = document.children()
    document.page_set = children.instance_of(Page)

    context = {
        "object": document,
        "parent": document.parent,
        "is_moderator": request.user.is_moderator(document.parent),
        "page_set": list(document.page_set.order_by('id').all()),
    }
    document.views = F('views') + 1
    document.save(update_fields=['views'])
    return render(request, "viewer.html", context)
<commit_msg>Fix download views for documents (use django file API)<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals

# Copyright 2014, Cercle Informatique ASBL. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This software was made by hast, C4, ititou at UrLab, ULB's hackerspace

import os
import uuid

from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.db.models import F

from documents.models import Document, Page
from graph.models import Course
from polydag.models import Node
from documents.forms import UploadFileForm, FileForm

from cycle import add_document_to_queue

@login_required
def upload_file(request, parent_id):
    parentNode = get_object_or_404(Node, id=parent_id)
    if not isinstance(parentNode, Course):
        raise NotImplementedError("Not a course")

    if request.method == 'POST':
        form = UploadFileForm(request.POST, request.FILES)

        if form.is_valid():
            if len(form.cleaned_data['name']) > 0:
                name = form.cleaned_data['name']
            else:
                name, _ = os.path.splitext(request.FILES['file'].name)
                name = name.lower()

            extension = os.path.splitext(request.FILES['file'].name)[1].lower()
            description = form.cleaned_data['description']
            course = parentNode

            doc = Document.objects.create(
                user=request.user,
                name=name,
                description=description,
                state="PREPARING",
                file_type=extension
            )
            doc.original.save(str(uuid.uuid4()), request.FILES['file'])
            doc.save()

            course.add_child(doc)

            doc.add_keywords(*form.cleaned_data['tags'])
            doc.year = form.cleaned_data['year']

            doc.state = 'READY_TO_QUEUE'
            doc.save()

            add_document_to_queue(doc)

            return HttpResponseRedirect(reverse('course_show', args=[course.slug]))

    else:
        form = UploadFileForm()

    return render(request, 'document_upload.html', {
        'form': form,
        'parent': parentNode,
    })

@login_required
def document_edit(request, document_id):
    doc = get_object_or_404(Document, id=document_id)

    if request.user != doc.user and not request.user.is_moderator(doc.parent):
        return HttpResponse('
403
', status=403)

    if request.method == 'POST':
        form = FileForm(request.POST)

        if form.is_valid():
            doc.name = form.cleaned_data['name']
            doc.description = form.cleaned_data['description']

            doc.keywords.clear()
            doc.add_keywords(*form.cleaned_data['tags'])

            doc.year = form.cleaned_data['year']
            doc.save()

            return HttpResponseRedirect(reverse('document_show', args=[doc.id]))

    else:
        form = FileForm({
            'name': doc.name,
            'description': doc.description,
            'year': doc.year,
            'tags': doc.keywords.all()
        })

    return render(request, 'document_edit.html', {
        'form': form,
        'doc': doc,
    })

@login_required
def document_download(request, id):
    doc = get_object_or_404(Document, id=id)
    body = doc.pdf.read()
    response = HttpResponse(body, content_type='application/pdf')
    response['Content-Disposition'] = 'attachment; filename="%s.pdf"' % (doc.name)
    doc.downloads += 1
    doc.save()
    return response

@login_required
def document_download_original(request, id):
    doc = get_object_or_404(Document, id=id)
    body = doc.original.read()
    response = HttpResponse(body, content_type='application/octet-stream')
    response['Content-Description'] = 'File Transfer'
    response['Content-Transfer-Encoding'] = 'binary'
    response['Content-Disposition'] = 'attachment; filename="{}{}"'.format(doc.name, doc.file_type)
    doc.downloads += 1
    doc.save()
    return response

@login_required
def document_show(request, id):
    document = get_object_or_404(Document, id=id)

    children = document.children()
    document.page_set = children.instance_of(Page)

    context = {
        "object": document,
        "parent": document.parent,
        "is_moderator": request.user.is_moderator(document.parent),
        "page_set": list(document.page_set.order_by('id').all()),
    }
    document.views = F('views') + 1
    document.save(update_fields=['views'])
    return render(request, "viewer.html", context)
<|endoftext|>


<commit_before>"""Dead code statistic and common errors statistic history generators.

Source data for all graphs are retrieved from the 'history repository' with
QA Dashboard and its data.
"""

import re
from config import Config
import git_utils
import history_generator

def line_with_summary(line, summary_postfix):
    """Check if the processed line contains dead code measurement summary."""
    return line.endswith(summary_postfix)

def parse_summary(line, pattern):
    """Parse the line containing dead code measurement summary."""
    pattern = re.compile(pattern)
    match = pattern.match(line)
    if len(match.groups()) == 2:
        return {"files_with_issues": match.group(1),
                "total_files": match.group(2)}
    else:
        return None

def line_with_check_passed(line, prefix):
    """Check if the processed line contains dead code measurement summary that passed."""
    return line.startswith(prefix)

def parse_check_passed(line, pattern):
    """Parse the line containing dead code measurement summary that passed."""
    pattern = re.compile(pattern)
    match = pattern.match(line)
    if len(match.groups()) == 1:
        return {"files_with_issues": 0,
                "total_files": match.group(1)}
    else:
        return None

def read_summary(filename, summary_postfix, summary_pattern, check_passed_prefix,
                 check_passed_pattern):
    """Read dead code summary from the file containing dead code or common errors report."""
    try:
        # it is ok if the file does not exist - the history data don't have to be
        # available in all commits
        with open(filename) as fin:
            for line in fin:
                if line_with_summary(line.strip(), summary_postfix):
                    return parse_summary(line, summary_pattern)
                elif line_with_check_passed(line.strip(), check_passed_prefix):
                    return parse_check_passed(line, check_passed_pattern)
    except Exception:
        return None

def get_filename_with_dead_code_stats(hist_repo, repo_to_measure):
    """Get the filename that contains dead code statistic."""
    return "repositories/{repo}/dashboard/{repo_to_measure}.dead_code.txt".format(
           repo=hist_repo, repo_to_measure=repo_to_measure)

def get_filename_with_common_errors_stats(hist_repo, repo_to_measure):
    """Get the filename that contains common errors statistic."""
    return "repositories/{repo}/dashboard/{repo_to_measure}.common_errors.txt".format(
           repo=hist_repo, repo_to_measure=repo_to_measure)

def read_history(hist_repo, commits, repo_to_measure, summary_postfix, summary_pattern,
                 checks_passed_prefix, checks_passed_pattern,
                 get_filename_function):
    """Read dead code history for the selected repository."""
    git_utils.checkout(hist_repo, "master")
    hist_repo = git_utils.update_repository_name(hist_repo)
    filename = get_filename_function(hist_repo, repo_to_measure)

    history = []

    # checkout to every commit and read the code coverage statistic
    for commit in commits:
        commit_hash = commit[0]
        commit_date = history_generator.get_commit_date(commit[1])
        git_utils.checkout(hist_repo, commit_hash)
        summary = read_summary(filename, summary_postfix, summary_pattern, checks_passed_prefix,
                               checks_passed_pattern)
        if summary is not None:
            summary["date"] = commit_date
            history.append(summary)

    return history

def compute_correct_files(all_files, incorrect_files):
    """Compute the sequence of 'correct' files."""
    return history_generator.diff_two_lists(all_files, incorrect_files)

def plot_series_to_graph(ax, history, all_files_label, incorrect_files_label, correct_files_label):
    """Plot all data series onto the graph."""
    # x-axis value
    x_axis = history_generator.get_values_as_str(history, "date")

    # y-axis values (series)
    files = history_generator.get_values_as_int(history, "total_files")
    incorrect = history_generator.get_values_as_int(history, "files_with_issues")
    correct = compute_correct_files(files, incorrect)

    ax.plot(x_axis, files, "b-", label=all_files_label)
    ax.plot(x_axis, incorrect, "r-", label=incorrect_files_label)
    ax.plot(x_axis, correct, "g-", label=correct_files_label)

def plot_dead_code_series_to_graph(ax, history):
    """Plot all data series onto the graph."""
    plot_series_to_graph(ax, history, "All files", "Detected dead code", "Files w/o dead code")

def plot_common_errors_series_to_graph(ax, history):
    """Plot all data series onto the graph."""
    plot_series_to_graph(ax, history, "All files", "Detected common errors",
                         "Files w/o common errors")

def generate_graph_with_dead_code(hist_repo, commits, repo_to_measure):
    """Generate graph with the overall dead code for the selected repository."""
    dead_code_history = read_history(hist_repo, commits, repo_to_measure,
                                     'seems to contain dead code and/or unused imports',
                                     r'(\d+) source files out of (\d+) files seems',
                                     'All checks passed for',
                                     r'All checks passed for (\d+) source files',
                                     get_filename_with_dead_code_stats)

    # there's no need to generate graph with no value or with only one value
    if dead_code_history is not None and len(dead_code_history) >= 1:
        title = "Dead code history for the " + repo_to_measure
        filename = "dead_code_history_{repo}.png".format(repo=repo_to_measure)
        history_generator.draw_graph(title, filename, dead_code_history,
                                     plot_dead_code_series_to_graph)

def generate_graph_with_common_errors(hist_repo, commits, repo_to_measure):
    """Generate graph with the overall common errors statistic for the selected repository."""
    common_errors_history = read_history(hist_repo, commits, repo_to_measure,
                                         'files needs to be checked and fixed',
                                         r'(\d+) source files out of (\d+) files needs',
                                         'All checks passed for',
                                         r'All checks passed for (\d+) source files',
                                         get_filename_with_common_errors_stats)

    # there's no need to generate graph with no value or with only one value
    if common_errors_history is not None and len(common_errors_history) >= 1:
        title = "Common errors history for the " + repo_to_measure
        filename = "common_errors_history_{repo}.png".format(repo=repo_to_measure)
        history_generator.draw_graph(title, filename, common_errors_history,
                                     plot_common_errors_series_to_graph)

def main():
    """Entry point to the dead code history generator."""
    config = Config()
    hist_repo = config.get_repo_with_history_data()
    history_generator.prepare_hist_repository(hist_repo)

    # generate graph for all supported repositories
    for repository in config.get_repolist():
        commits = history_generator.read_history_commits()
        generate_graph_with_dead_code(hist_repo, commits, repository)
        generate_graph_with_common_errors(hist_repo, commits, repository)

if __name__ == "__main__":
    # execute only if run as a script
    main()
<commit_msg>Refactoring: read code history in own functions<commit_after>"""Dead code statistic and common errors statistic history generators.

Source data for all graphs are retrieved from the 'history repository' with
QA Dashboard and its data.
"""

import re
from config import Config
import git_utils
import history_generator

def line_with_summary(line, summary_postfix):
    """Check if the processed line contains dead code measurement summary."""
    return line.endswith(summary_postfix)

def parse_summary(line, pattern):
    """Parse the line containing dead code measurement summary."""
    pattern = re.compile(pattern)
    match = pattern.match(line)
    if len(match.groups()) == 2:
        return {"files_with_issues": match.group(1),
                "total_files": match.group(2)}
    else:
        return None

def line_with_check_passed(line, prefix):
    """Check if the processed line contains dead code measurement summary that passed."""
    return line.startswith(prefix)

def parse_check_passed(line, pattern):
    """Parse the line containing dead code measurement summary that passed."""
    pattern = re.compile(pattern)
    match = pattern.match(line)
    if len(match.groups()) == 1:
        return {"files_with_issues": 0,
                "total_files": match.group(1)}
    else:
        return None

def read_summary(filename, summary_postfix, summary_pattern, check_passed_prefix,
                 check_passed_pattern):
    """Read dead code summary from the file containing dead code or common errors report."""
    try:
        # it is ok if the file does not exist - the history data don't have to be
        # available in all commits
        with open(filename) as fin:
            for line in fin:
                if line_with_summary(line.strip(), summary_postfix):
                    return parse_summary(line, summary_pattern)
                elif line_with_check_passed(line.strip(), check_passed_prefix):
                    return parse_check_passed(line, check_passed_pattern)
    except Exception:
        return None

def get_filename_with_dead_code_stats(hist_repo, repo_to_measure):
    """Get the filename that contains dead code statistic."""
    return "repositories/{repo}/dashboard/{repo_to_measure}.dead_code.txt".format(
           repo=hist_repo, repo_to_measure=repo_to_measure)

def get_filename_with_common_errors_stats(hist_repo, repo_to_measure):
    """Get the filename that contains common errors statistic."""
    return "repositories/{repo}/dashboard/{repo_to_measure}.common_errors.txt".format(
           repo=hist_repo, repo_to_measure=repo_to_measure)

def read_history(hist_repo, commits, repo_to_measure, summary_postfix, summary_pattern,
                 checks_passed_prefix, checks_passed_pattern,
                 get_filename_function):
    """Read dead code history for the selected repository."""
    git_utils.checkout(hist_repo, "master")
    hist_repo = git_utils.update_repository_name(hist_repo)
    filename = get_filename_function(hist_repo, repo_to_measure)

    history = []

    # checkout to every commit and read the code coverage statistic
    for commit in commits:
        commit_hash = commit[0]
        commit_date = history_generator.get_commit_date(commit[1])
        git_utils.checkout(hist_repo, commit_hash)
        summary = read_summary(filename, summary_postfix, summary_pattern, checks_passed_prefix,
                               checks_passed_pattern)
        if summary is not None:
            summary["date"] = commit_date
            history.append(summary)

    return history

def compute_correct_files(all_files, incorrect_files):
    """Compute the sequence of 'correct' files."""
    return history_generator.diff_two_lists(all_files, incorrect_files)

def plot_series_to_graph(ax, history, all_files_label, incorrect_files_label, correct_files_label):
    """Plot all data series onto the graph."""
    # x-axis value
    x_axis = history_generator.get_values_as_str(history, "date")

    # y-axis values (series)
    files = history_generator.get_values_as_int(history, "total_files")
    incorrect = history_generator.get_values_as_int(history, "files_with_issues")
    correct = compute_correct_files(files, incorrect)

    ax.plot(x_axis, files, "b-", label=all_files_label)
    ax.plot(x_axis, incorrect, "r-", label=incorrect_files_label)
    ax.plot(x_axis, correct, "g-", label=correct_files_label)

def plot_dead_code_series_to_graph(ax, history):
    """Plot all data series onto the graph."""
    plot_series_to_graph(ax, history, "All files", "Detected dead code", "Files w/o dead code")

def plot_common_errors_series_to_graph(ax, history):
    """Plot all data series onto the graph."""
    plot_series_to_graph(ax, history, "All files", "Detected common errors",
                         "Files w/o common errors")

def read_dead_code_history(hist_repo, commits, repo_to_measure):
    """Read dead code history for given repository."""
    return read_history(hist_repo, commits, repo_to_measure,
                        'seems to contain dead code and/or unused imports',
                        r'(\d+) source files out of (\d+) files seems',
                        'All checks passed for',
                        r'All checks passed for (\d+) source files',
                        get_filename_with_dead_code_stats)

def read_common_errors_history(hist_repo, commits, repo_to_measure):
    """Read common errors history for given repository."""
    return read_history(hist_repo, commits, repo_to_measure,
                        'files needs to be checked and fixed',
                        r'(\d+) source files out of (\d+) files needs',
                        'All checks passed for',
                        r'All checks passed for (\d+) source files',
                        get_filename_with_common_errors_stats)

def generate_graph_with_dead_code(repo_to_measure, dead_code_history):
    """Generate graph with the overall dead code for the selected repository."""
    # there's no need to generate graph with no value or with only one value
    if dead_code_history is not None and len(dead_code_history) >= 1:
        title = "Dead code history for the " + repo_to_measure
        filename = "dead_code_history_{repo}.png".format(repo=repo_to_measure)
        history_generator.draw_graph(title, filename, dead_code_history,
                                     plot_dead_code_series_to_graph)

def generate_graph_with_common_errors(repo_to_measure, common_errors_history):
    """Generate graph with the overall common errors statistic for the selected repository."""
    # there's no need to generate graph with no value or with only one value
    if common_errors_history is not None and len(common_errors_history) >= 1:
        title = "Common errors history for the " + repo_to_measure
        filename = "common_errors_history_{repo}.png".format(repo=repo_to_measure)
        history_generator.draw_graph(title, filename, common_errors_history,
                                     plot_common_errors_series_to_graph)

def main():
    """Entry point to the dead code history generator."""
    config = Config()
    hist_repo = config.get_repo_with_history_data()
    history_generator.prepare_hist_repository(hist_repo)
    repositories = config.get_repolist()

    # generate graph for all supported repositories
    for repository in repositories:
        commits = history_generator.read_history_commits()

        dead_code_history = read_dead_code_history(hist_repo, commits, repository)
        common_errors_history = read_common_errors_history(hist_repo, commits, repository)

        generate_graph_with_dead_code(repository, dead_code_history)
        generate_graph_with_common_errors(repository, common_errors_history)

if __name__ == "__main__":
    # execute only if run as a script
    main()
<|endoftext|>


<commit_before>__all__ = ['astnode', 'ASTNode', 'Symbol', 'children',
	   'reconstruct', 'makeASTManifest',
	   'astequal', 'asthash',]

# A metaclass for generating AST node classes.

import sys

from . import codegeneration

doTypeChecks = False

class ClassBuilder(object):
	def __init__(self, type, name, bases, d):
		self.type  = type
		self.name  = name
		self.bases = bases
		self.d     = d

	def getShared(self):
		shared = bool(self.d.get('__shared__', False))
		self.d['__shared__'] = shared
		return shared

	def getGlobalDict(self):
		module = self.d['__module__']
		assert module in sys.modules
		g = sys.modules[module].__dict__
		return g

	def getFields(self):
		# Process the fields
		if '__fields__' not in self.d:
			fields = ()
			self.d['__fields__'] = fields
		else:
			fields = self.d['__fields__']

			if isinstance(fields, str):
				fields = (fields,)
				self.d['__fields__'] = fields
		return fields

	def getTypes(self, fields):
		# Process the field types
		if '__types__' not in self.d:
			types = {}
			self.d['__types__'] = types
		else:
			types = self.d['__types__']

		assert isinstance(types, dict), types

		for k in types.iterkeys():
			assert k in fields, "Typed field %s is does not exist." % k

		return types

	def getOptional(self, fields):
		optional = self.d.get('__optional__', ())

		if isinstance(optional, str):
			optional = (optional,)

		optional = frozenset(optional)

		self.d['__optional__'] = optional

		for k in optional:
			assert k in fields, "Optional field %s is does not exist." % k

		return optional

	def makeWrappedFieldSlots(self, fields, types, optional):
		slots = ["_%s" % field for field in fields]

		for field in fields:
			getter = self.makeFunc(codegeneration.makeGetter, (self.name, field))
			setter = self.makeFunc(codegeneration.makeSetter, (self.name, field, types.get(field), field in optional))
			self.d[field] = property(getter, setter)

		return slots

	def makeDirectFieldSlots(self, fields, types, optional):
		return [field for field in fields]

	def makeFieldSlots(self, fields, types, optional):
		# Create slots for the fields.
		if doTypeChecks:
			return self.makeWrappedFieldSlots(fields, types, optional)
		else:
			return self.makeDirectFieldSlots(fields, types, optional)

	def appendToExistingSlots(self, slots):
		# Prepend the fields to the existing slots declaration.
		existing = self.d.get('__slots__', ())
		if isinstance(existing, str):
			existing = (existing,)

		slots.extend(existing)
		self.d['__slots__'] = tuple(slots)
		return slots

	def finalize(self):
		return type.__new__(self.type, self.name, self.bases, self.d)

	def makeFunc(self, func, args):
		return codegeneration.compileFunc(func(*args), self.g)

	def defaultFunc(self, name, func, args):
		if not name in self.d:
			self.d[name] = self.makeFunc(func, args)

	def addDefaultMethods(self, fields, types, optional):
		# Generate and attach methods.
		self.defaultFunc('__init__', codegeneration.makeInit, (self.name, fields, types, optional))
		self.defaultFunc('__repr__', codegeneration.makeRepr, (self.name, fields))
		self.defaultFunc('accept',   codegeneration.makeAccept, (self.name,))
		self.defaultFunc('children', codegeneration.makeGetChildren, (fields,))
		self.defaultFunc('fields',   codegeneration.makeGetFields, (fields,))

		self.defaultFunc('asteq', codegeneration.makeEq, (fields,))
		self.defaultFunc('asthash', codegeneration.makeHash, (fields,))

	def mutate(self):
		self.g   = self.getGlobalDict()

		fields   = self.getFields()
		types    = self.getTypes(fields)
		optional = self.getOptional(fields)

		shared   = self.getShared()

		slots = self.makeFieldSlots(fields, types, optional)
		slots = self.appendToExistingSlots(slots)

		self.addDefaultMethods(fields, types, optional)

		return self

	def build(self):
		return self.mutate().finalize()

# TODO grab fields from bases?

class astnode(type):
	def __new__(mcls, name, bases, d):
		return ClassBuilder(mcls, name, bases, d).build()

def makeASTManifest(glbls):
	manifest = {}
	for name, obj in glbls.iteritems():
		if isinstance(obj, type) and hasattr(obj, '__metaclass__'):
			if obj.__metaclass__ == astnode:
				manifest[name] = obj
	return manifest

LeafTypes = (str, type(None), bool, int, long, float)
ListTypes = (list, tuple)

def children(node):
	if isinstance(node, LeafTypes):
		return ()
	elif isinstance(node, ListTypes):
		return node
	else:
		return node.children()

def reconstruct(node, newchildren):
	if isinstance(node, LeafTypes):
		assert not newchildren, newchildren
		return node
	elif isinstance(node, ListTypes):
		return type(node)(newchildren)
	else:
		try:
			newnode = type(node)(*newchildren)
			assert hasattr(node, 'annotation'), node
			newnode.annotation = node.annotation
			return newnode
		except:
			print "error", node
			print newchildren
			raise

def astequal(a, b):
	if isinstance(a, LeafTypes) and isinstance(b, LeafTypes):
		return a == b
	elif type(a) is type(b):
		ca, cb = children(a), children(b)
		if len(ca) == len(cb):
			for x, y in zip(ca, cb):
				if not astequal(x, y):
					return False
			return True
	return False

def asthash(node):
	if isinstance(node, LeafTypes):
		return hash(node)
	else:
		return id(type(node))^hash(tuple(node.children))

class Symbol(object):
	__metaclass__ = astnode
	__slots__    = 'name'

	def __init__(self, name):
		assert isinstance(name, str)
		self.name = name

	def __repr__(self):
		return "Symbol(%s)" % self.name

class ASTNode(object):
	__metaclass__ = astnode
	__slots__ = 'annotation'

	emptyAnnotation = None

	def __init__(self):
		self.annotation = self.emptyAnnotation

	def rewriteAnnotation(self, **kwds):
		self.annotation = self.annotation.rewrite(**kwds)
<commit_msg>ASTNodes now parses __fields__ and __optional__ if they are strings.<commit_after>__all__ = ['astnode', 'ASTNode', 'Symbol', 'children',
	   'reconstruct', 'makeASTManifest',
	   'astequal', 'asthash',]

# A metaclass for generating AST node classes.

import sys

from . import codegeneration

doTypeChecks = False

class ClassBuilder(object):
	def __init__(self, type, name, bases, d):
		self.type  = type
		self.name  = name
		self.bases = bases
		self.d     = d

	def getShared(self):
		shared = bool(self.d.get('__shared__', False))
		self.d['__shared__'] = shared
		return shared

	def getGlobalDict(self):
		module = self.d['__module__']
		assert module in sys.modules
		g = sys.modules[module].__dict__
		return g

	def getFields(self):
		# Process the fields
		if '__fields__' not in self.d:
			fields = ()
			self.d['__fields__'] = fields
		else:
			fields = self.d['__fields__']

			if isinstance(fields, str):
				fields = tuple(fields.strip().split())
				self.d['__fields__'] = fields
		return fields

	def getTypes(self, fields):
		# Process the field types
		if '__types__' not in self.d:
			types = {}
			self.d['__types__'] = types
		else:
			types = self.d['__types__']

		assert isinstance(types, dict), types

		for k in types.iterkeys():
			assert k in fields, "Typed field %r is does not exist." % k

		return types

	def getOptional(self, fields):
		optional = self.d.get('__optional__', ())

		if isinstance(optional, str):
			optional =  tuple(optional.strip().split())

		optional = frozenset(optional)

		self.d['__optional__'] = optional

		for k in optional:
			assert k in fields, "Optional field %s is does not exist." % k

		return optional

	def makeWrappedFieldSlots(self, fields, types, optional):
		slots = ["_%s" % field for field in fields]

		for field in fields:
			getter = self.makeFunc(codegeneration.makeGetter, (self.name, field))
			setter = self.makeFunc(codegeneration.makeSetter, (self.name, field, types.get(field), field in optional))
			self.d[field] = property(getter, setter)

		return slots

	def makeDirectFieldSlots(self, fields, types, optional):
		return [field for field in fields]

	def makeFieldSlots(self, fields, types, optional):
		# Create slots for the fields.
		if doTypeChecks:
			return self.makeWrappedFieldSlots(fields, types, optional)
		else:
			return self.makeDirectFieldSlots(fields, types, optional)

	def appendToExistingSlots(self, slots):
		# Prepend the fields to the existing slots declaration.
		existing = self.d.get('__slots__', ())
		if isinstance(existing, str):
			existing = (existing,)

		slots.extend(existing)
		self.d['__slots__'] = tuple(slots)
		return slots

	def finalize(self):
		return type.__new__(self.type, self.name, self.bases, self.d)

	def makeFunc(self, func, args):
		return codegeneration.compileFunc(func(*args), self.g)

	def defaultFunc(self, name, func, args):
		if not name in self.d:
			self.d[name] = self.makeFunc(func, args)

	def addDefaultMethods(self, fields, types, optional):
		# Generate and attach methods.
		self.defaultFunc('__init__', codegeneration.makeInit, (self.name, fields, types, optional))
		self.defaultFunc('__repr__', codegeneration.makeRepr, (self.name, fields))
		self.defaultFunc('accept',   codegeneration.makeAccept, (self.name,))
		self.defaultFunc('children', codegeneration.makeGetChildren, (fields,))
		self.defaultFunc('fields',   codegeneration.makeGetFields, (fields,))

		self.defaultFunc('asteq', codegeneration.makeEq, (fields,))
		self.defaultFunc('asthash', codegeneration.makeHash, (fields,))

	def mutate(self):
		self.g   = self.getGlobalDict()

		fields   = self.getFields()
		types    = self.getTypes(fields)
		optional = self.getOptional(fields)

		shared   = self.getShared()

		slots = self.makeFieldSlots(fields, types, optional)
		slots = self.appendToExistingSlots(slots)

		self.addDefaultMethods(fields, types, optional)

		return self

	def build(self):
		return self.mutate().finalize()

# TODO grab fields from bases?

class astnode(type):
	def __new__(mcls, name, bases, d):
		return ClassBuilder(mcls, name, bases, d).build()

def makeASTManifest(glbls):
	manifest = {}
	for name, obj in glbls.iteritems():
		if isinstance(obj, type) and hasattr(obj, '__metaclass__'):
			if obj.__metaclass__ == astnode:
				manifest[name] = obj
	return manifest

LeafTypes = (str, type(None), bool, int, long, float)
ListTypes = (list, tuple)

def children(node):
	if isinstance(node, LeafTypes):
		return ()
	elif isinstance(node, ListTypes):
		return node
	else:
		return node.children()

def reconstruct(node, newchildren):
	if isinstance(node, LeafTypes):
		assert not newchildren, newchildren
		return node
	elif isinstance(node, ListTypes):
		return type(node)(newchildren)
	else:
		try:
			newnode = type(node)(*newchildren)
			assert hasattr(node, 'annotation'), node
			newnode.annotation = node.annotation
			return newnode
		except:
			print "error", node
			print newchildren
			raise

def astequal(a, b):
	if isinstance(a, LeafTypes) and isinstance(b, LeafTypes):
		return a == b
	elif type(a) is type(b):
		ca, cb = children(a), children(b)
		if len(ca) == len(cb):
			for x, y in zip(ca, cb):
				if not astequal(x, y):
					return False
			return True
	return False

def asthash(node):
	if isinstance(node, LeafTypes):
		return hash(node)
	else:
		return id(type(node))^hash(tuple(node.children))

class Symbol(object):
	__metaclass__ = astnode
	__slots__    = 'name'

	def __init__(self, name):
		assert isinstance(name, str)
		self.name = name

	def __repr__(self):
		return "Symbol(%s)" % self.name

class ASTNode(object):
	__metaclass__ = astnode
	__slots__ = 'annotation'

	emptyAnnotation = None

	def __init__(self):
		self.annotation = self.emptyAnnotation

	def rewriteAnnotation(self, **kwds):
		self.annotation = self.annotation.rewrite(**kwds)
<|endoftext|>


<commit_before>import gtk
import cairo

# -- Base classes for drawing sound visualization.
#
# CairoWidget, LayeredCairoWidget, and LayeredGraphView are defined as
# successive subclasses only for code clarity. Everything could as
# well be stuck in LayeredGraphView.
#
class CairoWidget(gtk.DrawingArea):

    __gsignals__ = {"expose-event": "override"}

    def __init__(self):
        super(CairoWidget, self).__init__()

    def do_expose_event(self, event):
        context = self.window.cairo_create()
        context.rectangle(event.area.x, event.area.y,
                          event.area.width, event.area.height)
        context.clip()
        width, height = self.window.get_size()
        self.draw(context, width, height)

    def redraw(self):
        # queue_draw() emits an expose event. Double buffering is used
        # automatically in the expose event handler.
        self.queue_draw()

    def draw(self, context, width, height):
        """Must be overriden to draw to the cairo context."""
        pass

class LayeredCairoWidget(CairoWidget):
    """A widget with several layers.

    This widget paints itself by successively passing its context to
    layer objects. The draw() method of a layer object must paint to
    the context.

    """
    def __init__(self):
        super(LayeredCairoWidget, self).__init__()
        self.layers = []

    def draw(self, context, width, height):
        for layer in self.layers:
            layer.draw(context, width, height)

class LayeredGraphView(LayeredCairoWidget):
    """A layered widget dedicated to a Graph object.

    Every time the widget is resized, the new width is passed to the
    Graph object.

    """
    def __init__(self, graph):
        super(LayeredGraphView, self).__init__()
        self._graph = graph
        self.connect("size_allocate", self.resized)

    def resized(self, widget, rect):
        self._graph.set_width(rect.width)

# -- The sound visualization widget, composed of several layers:
#
#    * waveform
#    * selection
#
class GraphView(LayeredGraphView):
    """Sound visualization widget for the main window.

    * Two graphical layers: the waveform and the selection.
    * Mouse event listeners act on models (scroll and selection).

    """
    def __init__(self, graph, selection):
        super(GraphView, self).__init__(graph)
        self._graph = graph
        self._selection = selection
        self.layers.append(WaveformLayer(self, graph))
        self.layers.append(SelectionLayer(self, selection))
        MouseSelection(self, selection)
        MouseScroll(self, graph)

# -- Layers that can be added to LayeredGraphview.
#
class WaveformLayer(object):
    """A layer for LayeredGraphView.

    It paints the graph (the waveform). The cairo surface is cached,
    it is redrawn only when graph has changed.

    """
    def __init__(self, layered, graph):
        self._layered = layered
        self._graph = graph
        self._cache = None
        layered.add_events(gtk.gdk.SCROLL_MASK)
        graph.changed.connect(self.update)

    def update(self):
        self._cache = None
        self._layered.redraw()

    def draw_channel(self, values, surface, width, height):
        "Draw one sound channel on a cairo surface."
        c = cairo.Context(surface)

        # Line at zero
        c.set_line_width(1)
        c.set_source_rgb(0.2, 0.2, 0.2)
        c.move_to(0, round(height / 2) + 0.5)
        c.line_to(width, round(height / 2) + 0.5)
        c.stroke()

        # Waveform
        c.set_source_rgb(0, 0.9, 0)
        for i, (mini, maxi) in enumerate(values):
            # -1 <= mini <= maxi <= 1
            x = i
            ymin = round((-mini * 0.5 + 0.5) * height)
            ymax = round((-maxi * 0.5 + 0.5) * height)
            if ymin == ymax:
                # Fill one pixel
                c.rectangle(x, ymin, 1, 1)
                c.fill()
            else:
                # Draw a line from min to max
                c.move_to(x + 0.5, ymin + 0.5)
                c.line_to(x + 0.5, ymax + 0.5)
                c.stroke()

    def draw(self, context, width, height):
        "Draw all sound channels."
        if not self._cache:
            surface = context.get_target()
            self._cache = surface.create_similar(cairo.CONTENT_COLOR,
                                                 width, height)
            c = cairo.Context(self._cache)
            channels = self._graph.channels()
            numchan = len(channels)
            for i in range(numchan):
                s = surface.create_similar(cairo.CONTENT_COLOR,
                                                     width, height / numchan)
                self.draw_channel(channels[i], s, width, height / numchan)
                c.set_source_surface(s, 0, (height / numchan) * i)
                c.set_operator(cairo.OPERATOR_ATOP)
                c.paint()

        context.set_source_surface(self._cache, 0, 0)
        context.set_operator(cairo.OPERATOR_SOURCE)
        context.paint()

class SelectionLayer(object):
    """A layer for LayeredGraphView.

    It highlights the selected area. It also listens to mouse events
    and changes the selection accordingly. The cairo surface is
    cached, it is redrawn only when selection has changed.

    """
    def __init__(self, layered, selection):
        self._layered = layered
        self._cache = None
        self._selection = selection
        self._selection.changed.connect(self.update)

    def update(self):
        self._cache = None
        self._layered.redraw()

    def draw(self, context, width, height):
        if not self._cache:
            surface = context.get_target()
            self._cache = surface.create_similar(cairo.CONTENT_COLOR_ALPHA,
                                                 width, height)
            c = cairo.Context(self._cache)
            start, end = self._selection.pixels()

            if start != end:
                # darken everything ...
                c.set_source_rgba(0, 0, 0, 0.75)
                c.paint()

                # ... then clear selection
                c.set_operator(cairo.OPERATOR_CLEAR)
                c.rectangle(start, 0, end - start, height)
                c.fill()

        context.set_source_surface(self._cache, 0, 0)
        context.set_operator(cairo.OPERATOR_OVER)
        context.paint()

# -- Mouse event listeners that act on models.
#
class MouseScroll(object):
    """Listens for mouse wheel events and scroll a graph.

    Must be attached to a gtk.Widget and a Graph.

    """
    def __init__(self, widget, graph):
        self._graph = graph
        widget.connect("scroll_event", self.scroll_event)

    def scroll_event(self, widget, event):
        if event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_LEFT):
            self._graph.scroll_left()
        elif event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_RIGHT):
            self._graph.scroll_right()

class MouseSelection(object):
    """Listens for mouse events and select graph area.

    Must be attached to a gtk.Widget and a Selection.

    """
    def __init__(self, widget, selection):
        self._selection = selection
        self.pressed = False
        widget.add_events(gtk.gdk.BUTTON_PRESS_MASK |
                          gtk.gdk.BUTTON_RELEASE_MASK |
                          gtk.gdk.POINTER_MOTION_MASK |
                          gtk.gdk.POINTER_MOTION_HINT_MASK)
        widget.connect("button_press_event", self.button_press)
        widget.connect("button_release_event", self.button_release)
        widget.connect("motion_notify_event", self.motion_notify)

    def button_press(self, widget, event):
        if event.button == 1:
            self.pressed = True
            self._selection.start_selection(event.x)

    def motion_notify(self, widget, event):
        if self.pressed:
            x = event.window.get_pointer()[0]
            self._selection.end_selection(x)

    def button_release(self, widget, event):
        if event.button == 1:
            self.pressed = False

# -- Horizontal scrollbar, subclassed to control a Graph object.
#
class GraphScrollbar(gtk.HScrollbar):
    """An horizontal scrollbar tied to a Graph.

    Acts on a graph object and is updated when the graph
    object changes.

    """
    def __init__(self, graph):
        self._adjustment = gtk.Adjustment(0, 0, 1, 0.1, 0, 1)
        super(GraphScrollbar, self).__init__(self._adjustment)
        self._graph = graph
        self._graph.changed.connect(self.update_scrollbar)
        self.connect("value-changed", self.update_model)

        # When the scrollbar or the graph model changes, this
        # attribute must be set to True to avoid infinite feedback
        # between them. Example of what happens otherwise: scrollbar
        # changes --> graph changes --> scrollbar changes --> ...
        self.inhibit = False

    def update_model(self, widget):
        """Changes the model.

        Called when the scrollbar has been moved by the user.

        """
        if not self.inhibit:
            self.inhibit = True
            self._graph.move_to(self._adjustment.value)
            self.inhibit = False

    def update_scrollbar(self):
        """Changes the scrollbar.

        Called when the model has changed.

        """
        if not self.inhibit:
            self.inhibit = True
            length = self._graph.numframes()
            start, end = self._graph.view()
            if start != end:
                self._adjustment.upper = length
                self._adjustment.value = start
                self._adjustment.page_size = (end - start)
            else:
                # empty sound
                self._adjustment.upper = 1
                self._adjustment.value = 0
                self._adjustment.page_size = 1
            self.inhibit = False

# -- Tests

if __name__ == '__main__':
    from mock import Mock, Fake

    def test_layered():

        def randomized():
            from random import random
            channels = [[((random() - 0.5) * 2, (random() - 0.5) * 2)
                       for i in xrange(500)]]
            graph = Mock({"channels": channels,
                          "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def sine():
            from math import sin
            sine = [sin(2 * 3.14 * 0.01 * x) for x in xrange(500)]
            channels = [[(i, i) for i in sine]]
            graph = Mock({"channels": channels, "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def sines():
            from math import sin
            sine = [sin(2 * 3.14 * 0.01 * x) for x in xrange(500)]
            channels = [[(i, i) for i in sine], [(i, i) for i in sine]]

            graph = Mock({"channels": channels, "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def selection():
            graph = Mock({"channels": [], "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            selection = Mock({"pixels": (20, 100)})
            selection.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(SelectionLayer(layered, selection))
            return layered

        layereds = [randomized(), sine(), sines(), selection()]

        for layered in layereds:
            window = gtk.Window()
            window.resize(500, 200)
            window.connect("delete-event", gtk.main_quit)
            window.add(layered)
            window.show_all()
            gtk.main()

    test_layered()
<commit_msg>whitespace<commit_after>import gtk
import cairo

# -- Base classes for drawing sound visualization.
#
# CairoWidget, LayeredCairoWidget, and LayeredGraphView are defined as
# successive subclasses only for code clarity. Everything could as
# well be stuck in LayeredGraphView.
#
class CairoWidget(gtk.DrawingArea):

    __gsignals__ = {"expose-event": "override"}

    def __init__(self):
        super(CairoWidget, self).__init__()

    def do_expose_event(self, event):
        context = self.window.cairo_create()
        context.rectangle(event.area.x, event.area.y,
                          event.area.width, event.area.height)
        context.clip()
        width, height = self.window.get_size()
        self.draw(context, width, height)

    def redraw(self):
        # queue_draw() emits an expose event. Double buffering is used
        # automatically in the expose event handler.
        self.queue_draw()

    def draw(self, context, width, height):
        """Must be overriden to draw to the cairo context."""
        pass

class LayeredCairoWidget(CairoWidget):
    """A widget with several layers.

    This widget paints itself by successively passing its context to
    layer objects. The draw() method of a layer object must paint to
    the context.

    """
    def __init__(self):
        super(LayeredCairoWidget, self).__init__()
        self.layers = []

    def draw(self, context, width, height):
        for layer in self.layers:
            layer.draw(context, width, height)

class LayeredGraphView(LayeredCairoWidget):
    """A layered widget dedicated to a Graph object.

    Every time the widget is resized, the new width is passed to the
    Graph object.

    """
    def __init__(self, graph):
        super(LayeredGraphView, self).__init__()
        self._graph = graph
        self.connect("size_allocate", self.resized)

    def resized(self, widget, rect):
        self._graph.set_width(rect.width)

# -- The sound visualization widget, composed of several layers:
#
#    * waveform
#    * selection
#
class GraphView(LayeredGraphView):
    """Sound visualization widget for the main window.

    * Two graphical layers: the waveform and the selection.
    * Mouse event listeners act on models (scroll and selection).

    """
    def __init__(self, graph, selection):
        super(GraphView, self).__init__(graph)
        self._graph = graph
        self._selection = selection
        self.layers.append(WaveformLayer(self, graph))
        self.layers.append(SelectionLayer(self, selection))
        MouseSelection(self, selection)
        MouseScroll(self, graph)

# -- Layers that can be added to LayeredGraphview.
#
class WaveformLayer(object):
    """A layer for LayeredGraphView.

    It paints the graph (the waveform). The cairo surface is cached,
    it is redrawn only when graph has changed.

    """
    def __init__(self, layered, graph):
        self._layered = layered
        self._graph = graph
        self._cache = None
        layered.add_events(gtk.gdk.SCROLL_MASK)
        graph.changed.connect(self.update)

    def update(self):
        self._cache = None
        self._layered.redraw()

    def draw_channel(self, values, surface, width, height):
        "Draw one sound channel on a cairo surface."
        c = cairo.Context(surface)

        # Line at zero
        c.set_line_width(1)
        c.set_source_rgb(0.2, 0.2, 0.2)
        c.move_to(0, round(height / 2) + 0.5)
        c.line_to(width, round(height / 2) + 0.5)
        c.stroke()

        # Waveform
        c.set_source_rgb(0, 0.9, 0)
        for i, (mini, maxi) in enumerate(values):
            # -1 <= mini <= maxi <= 1
            x = i
            ymin = round((-mini * 0.5 + 0.5) * height)
            ymax = round((-maxi * 0.5 + 0.5) * height)
            if ymin == ymax:
                # Fill one pixel
                c.rectangle(x, ymin, 1, 1)
                c.fill()
            else:
                # Draw a line from min to max
                c.move_to(x + 0.5, ymin + 0.5)
                c.line_to(x + 0.5, ymax + 0.5)
                c.stroke()

    def draw(self, context, width, height):
        "Draw all sound channels."
        if not self._cache:
            surface = context.get_target()
            self._cache = surface.create_similar(cairo.CONTENT_COLOR,
                                                 width, height)
            c = cairo.Context(self._cache)
            channels = self._graph.channels()
            numchan = len(channels)
            for i in range(numchan):
                s = surface.create_similar(cairo.CONTENT_COLOR,
                                           width, height / numchan)
                self.draw_channel(channels[i], s, width, height / numchan)
                c.set_source_surface(s, 0, (height / numchan) * i)
                c.set_operator(cairo.OPERATOR_ATOP)
                c.paint()

        context.set_source_surface(self._cache, 0, 0)
        context.set_operator(cairo.OPERATOR_SOURCE)
        context.paint()

class SelectionLayer(object):
    """A layer for LayeredGraphView.

    It highlights the selected area. It also listens to mouse events
    and changes the selection accordingly. The cairo surface is
    cached, it is redrawn only when selection has changed.

    """
    def __init__(self, layered, selection):
        self._layered = layered
        self._cache = None
        self._selection = selection
        self._selection.changed.connect(self.update)

    def update(self):
        self._cache = None
        self._layered.redraw()

    def draw(self, context, width, height):
        if not self._cache:
            surface = context.get_target()
            self._cache = surface.create_similar(cairo.CONTENT_COLOR_ALPHA,
                                                 width, height)
            c = cairo.Context(self._cache)
            start, end = self._selection.pixels()

            if start != end:
                # darken everything ...
                c.set_source_rgba(0, 0, 0, 0.75)
                c.paint()

                # ... then clear selection
                c.set_operator(cairo.OPERATOR_CLEAR)
                c.rectangle(start, 0, end - start, height)
                c.fill()

        context.set_source_surface(self._cache, 0, 0)
        context.set_operator(cairo.OPERATOR_OVER)
        context.paint()

# -- Mouse event listeners that act on models.
#
class MouseScroll(object):
    """Listens for mouse wheel events and scroll a graph.

    Must be attached to a gtk.Widget and a Graph.

    """
    def __init__(self, widget, graph):
        self._graph = graph
        widget.connect("scroll_event", self.scroll_event)

    def scroll_event(self, widget, event):
        if event.direction in (gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_LEFT):
            self._graph.scroll_left()
        elif event.direction in (gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_RIGHT):
            self._graph.scroll_right()

class MouseSelection(object):
    """Listens for mouse events and select graph area.

    Must be attached to a gtk.Widget and a Selection.

    """
    def __init__(self, widget, selection):
        self._selection = selection
        self.pressed = False
        widget.add_events(gtk.gdk.BUTTON_PRESS_MASK |
                          gtk.gdk.BUTTON_RELEASE_MASK |
                          gtk.gdk.POINTER_MOTION_MASK |
                          gtk.gdk.POINTER_MOTION_HINT_MASK)
        widget.connect("button_press_event", self.button_press)
        widget.connect("button_release_event", self.button_release)
        widget.connect("motion_notify_event", self.motion_notify)

    def button_press(self, widget, event):
        if event.button == 1:
            self.pressed = True
            self._selection.start_selection(event.x)

    def motion_notify(self, widget, event):
        if self.pressed:
            x = event.window.get_pointer()[0]
            self._selection.end_selection(x)

    def button_release(self, widget, event):
        if event.button == 1:
            self.pressed = False

# -- Horizontal scrollbar, subclassed to control a Graph object.
#
class GraphScrollbar(gtk.HScrollbar):
    """An horizontal scrollbar tied to a Graph.

    Acts on a graph object and is updated when the graph
    object changes.

    """
    def __init__(self, graph):
        self._adjustment = gtk.Adjustment(0, 0, 1, 0.1, 0, 1)
        super(GraphScrollbar, self).__init__(self._adjustment)
        self._graph = graph
        self._graph.changed.connect(self.update_scrollbar)
        self.connect("value-changed", self.update_model)

        # When the scrollbar or the graph model changes, this
        # attribute must be set to True to avoid infinite feedback
        # between them. Example of what happens otherwise: scrollbar
        # changes --> graph changes --> scrollbar changes --> ...
        self.inhibit = False

    def update_model(self, widget):
        """Changes the model.

        Called when the scrollbar has been moved by the user.

        """
        if not self.inhibit:
            self.inhibit = True
            self._graph.move_to(self._adjustment.value)
            self.inhibit = False

    def update_scrollbar(self):
        """Changes the scrollbar.

        Called when the model has changed.

        """
        if not self.inhibit:
            self.inhibit = True
            length = self._graph.numframes()
            start, end = self._graph.view()
            if start != end:
                self._adjustment.upper = length
                self._adjustment.value = start
                self._adjustment.page_size = (end - start)
            else:
                # empty sound
                self._adjustment.upper = 1
                self._adjustment.value = 0
                self._adjustment.page_size = 1
            self.inhibit = False

# -- Tests

if __name__ == '__main__':
    from mock import Mock, Fake

    def test_layered():

        def randomized():
            from random import random
            channels = [[((random() - 0.5) * 2, (random() - 0.5) * 2)
                       for i in xrange(500)]]
            graph = Mock({"channels": channels,
                          "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def sine():
            from math import sin
            sine = [sin(2 * 3.14 * 0.01 * x) for x in xrange(500)]
            channels = [[(i, i) for i in sine]]
            graph = Mock({"channels": channels, "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def sines():
            from math import sin
            sine = [sin(2 * 3.14 * 0.01 * x) for x in xrange(500)]
            channels = [[(i, i) for i in sine], [(i, i) for i in sine]]

            graph = Mock({"channels": channels, "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(WaveformLayer(layered, graph))
            return layered

        def selection():
            graph = Mock({"channels": [], "set_width": None,
                          "frames_info": (0, 0, 0)})
            graph.changed = Fake()
            selection = Mock({"pixels": (20, 100)})
            selection.changed = Fake()
            layered = LayeredGraphView(graph)
            layered.layers.append(SelectionLayer(layered, selection))
            return layered

        layereds = [randomized(), sine(), sines(), selection()]

        for layered in layereds:
            window = gtk.Window()
            window.resize(500, 200)
            window.connect("delete-event", gtk.main_quit)
            window.add(layered)
            window.show_all()
            gtk.main()

    test_layered()
<|endoftext|>