]> git.parisson.com Git - mezzo.git/commitdiff
Media [wip]: list audio video | issue with order by
authorEmilie <zawadzki@ircam.fr>
Fri, 30 Sep 2016 10:40:23 +0000 (12:40 +0200)
committerEmilie <zawadzki@ircam.fr>
Fri, 30 Sep 2016 10:40:23 +0000 (12:40 +0200)
app/organization/core/utils.py [new file with mode: 0644]
app/organization/media/migrations/0003_auto_20160930_1039.py [new file with mode: 0644]
app/organization/media/models.py
app/organization/media/urls.py
app/organization/media/views.py
app/templates/media/media_list.html [new file with mode: 0644]

diff --git a/app/organization/core/utils.py b/app/organization/core/utils.py
new file mode 100644 (file)
index 0000000..5c2c918
--- /dev/null
@@ -0,0 +1,237 @@
+from itertools import chain, dropwhile
+from operator import mul, attrgetter, __not__
+
+from django.db.models.query import REPR_OUTPUT_SIZE, EmptyQuerySet
+
+# Django Snippets
+# https://djangosnippets.org/snippets/1933/
+
+def mul_it(it1, it2):
+    '''
+    Element-wise iterables multiplications.
+    '''
+    assert len(it1) == len(it2),\
+           "Can not element-wise multiply iterables of different length."
+    return map(mul, it1, it2)
+
+
+def chain_sing(*iterables_or_items):
+    '''
+    As itertools.chain except that if an argument is not iterable then chain it
+    as a singleton.
+    '''
+    for iter_or_item in iterables_or_items:
+        if hasattr(iter_or_item, '__iter__'):
+            for item in iter_or_item:
+                yield item
+        else:
+            yield iter_or_item
+
+
+class IableSequence(object):
+    '''
+    Wrapper for sequence of iterable and indexable by non-negative integers
+    objects. That is a sequence of objects which implement __iter__, __len__ and
+    __getitem__ for slices, ints and longs.
+
+    Note: not a Django-specific class.
+    '''
+    def __init__(self, *args, **kwargs):
+        self.iables = args # wrapped sequence
+        self._len = None # length cache
+        self._collapsed = [] # collapsed elements cache
+
+    def __len__(self):
+        if not self._len:
+            self._len = sum(len(iable) for iable in self.iables)
+        return self._len
+
+
+    def __iter__(self):
+        return chain(*self.iables)
+
+    def __nonzero__(self):
+        try:
+            iter(self).__next__()
+        except StopIteration:
+            return False
+        return True
+
+
+    def _collect(self, start=0, stop=None, step=1):
+        if not stop:
+            stop = len(self)
+        sub_iables = []
+        # collect sub sets
+        it = self.iables.__iter__()
+        try:
+            while stop>start:
+                i = it.__next__()
+                i_len = len(i)
+                if i_len > start:
+                    # no problem with 'stop' being too big
+                    sub_iables.append(i[start:stop:step])
+                start = max(0, start-i_len)
+                stop -= i_len
+        except StopIteration:
+            pass
+        return sub_iables
+
+    def __getitem__(self, key):
+        '''
+        Preserves wrapped indexable sequences.
+        Does not support negative indices.
+        '''
+        # params validation
+        if not isinstance(key, (slice, int, long)):
+            raise TypeError
+        assert ((not isinstance(key, slice) and (key >= 0))
+                or (isinstance(key, slice) and (key.start is None or key.start >= 0)
+                    and (key.stop is None or key.stop >= 0))), \
+                "Negative indexing is not supported."
+        # initialization
+        if isinstance(key, slice):
+            start, stop, step = key.indices(len(self))
+            ret_item=False
+        else: # isinstance(key, (int,long))
+            start, stop, step = key, key+1, 1
+            ret_item=True
+        # collect sub sets
+        ret_iables = self._collect(start, stop, step)
+        # return the simplest possible answer
+        if not len(ret_iables):
+            if ret_item:
+                raise IndexError("'%s' index out of range" % self.__class__.__name__)
+            return ()
+        if ret_item:
+            # we have exactly one query set with exactly one item
+            assert len(ret_iables) == 1 and len(ret_iables[0]) == 1
+            return ret_iables[0][0]
+        # otherwise we have more then one item in at least one query set
+        if len(ret_iables) == 1:
+            return ret_iables[0]
+        # Note: this can't be self.__class__ instead of IableSequence; exemplary
+        # cause is that indexing over query sets returns lists so we can not
+        # return QuerySetSequence by default. Some type checking enhancement can
+        # be implemented in subclasses.
+        return IableSequence(*ret_iables)
+
+
+    def collapse(self, stop=None):
+        '''
+        Collapses sequence into a list.
+
+        Try to do it effectively with caching.
+        '''
+        if not stop:
+            stop = len(self)
+        # if we already calculated sufficient collapse then return it
+        if len(self._collapsed) >= stop:
+            return self._collapsed[:stop]
+        # otherwise collapse only the missing part
+        items = self._collapsed
+        sub_iables = self._collect(len(self._collapsed), stop)
+        for sub_iable in sub_iables:
+            items+=sub_iable
+        # cache new collapsed items
+        self._collapsed = items
+        return self._collapsed
+
+    def __repr__(self):
+        # get +1 element for the truncation msg if applicable
+        items = self.collapse(stop=REPR_OUTPUT_SIZE+1)
+        if len(items) > REPR_OUTPUT_SIZE:
+            items[-1] = "...(remaining elements truncated)..."
+        return repr(items)
+
+
+class QuerySetSequence(IableSequence):
+    '''
+    Wrapper for the query sets sequence without the restriction on the identity
+    of the base models.
+    '''
+    def count(self):
+        if not self._len:
+            self._len = sum(qs.count() for qs in self.iables)
+        return self._len
+
+    def __len__(self):
+        # override: use DB effective count's instead of len()
+        return self.count()
+
+    def order_by(self, *field_names):
+        '''
+        Returns a list of the QuerySetSequence items with the ordering changed.
+        '''
+        # construct a comparator function based on the field names prefixes
+        reverses = [1] * len(field_names)
+        field_names = list(field_names)
+        for i in range(len(field_names)):
+            field_name = field_names[i]
+            if field_name[0] == '-':
+                reverses[i] = -1
+                field_names[i] = field_name[1:]
+        # wanna iterable and attrgetter returns single item if 1 arg supplied
+        fields_getter = lambda i: chain_sing(attrgetter(*field_names)(i))
+        # comparator gets the first non-zero value of the field comparison
+        # results taking into account reverse order for fields prefixed with '-'
+        comparator = lambda i1, i2:\
+            dropwhile(__not__,
+              mul_it(map(cmp, fields_getter(i1), fields_getter(i2)), reverses)
+            ).__next__()
+        print("-----------------------------------------");
+        print(comparator);
+        print("******************************************");
+        print("comparator");
+
+        # return new sorted list
+        return sorted(self.collapse(), cmp=comparator)
+
+    def filter(self, *args, **kwargs):
+        """
+        Returns a new QuerySetSequence or instance with the args ANDed to the
+        existing set.
+
+        QuerySetSequence is simplified thus result actually can be one of:
+        QuerySetSequence, QuerySet, EmptyQuerySet.
+        """
+        return self._filter_or_exclude(False, *args, **kwargs)
+
+    def exclude(self, *args, **kwargs):
+        """
+        Returns a new QuerySetSequence instance with NOT (args) ANDed to the
+        existing set.
+
+        QuerySetSequence is simplified thus result actually can be one of:
+        QuerySetSequence, QuerySet, EmptyQuerySet.
+        """
+        return self._filter_or_exclude(True, *args, **kwargs)
+
+    def _simplify(self, qss=None):
+        '''
+        Returns QuerySetSequence, QuerySet or EmptyQuerySet depending on the
+        contents of items, i.e. at least two non empty QuerySets, exactly one
+        non empty QuerySet and all empty QuerySets respectively.
+
+        Does not modify original QuerySetSequence.
+        '''
+        not_empty_qss = filter(None, qss if qss else self.iables)
+        if not len(not_empty_qss):
+            return EmptyQuerySet()
+        if len(not_empty_qss) == 1:
+            return not_empty_qss[0]
+        return QuerySetSequence(*not_empty_qss)
+
+    def _filter_or_exclude(self, negate, *args, **kwargs):
+        '''
+        Maps _filter_or_exclude over QuerySet items and simplifies the result.
+        '''
+        # each Query set is cloned separately
+        return self._simplify(*map(lambda qs:
+            qs._filter_or_exclude(negate, *args, **kwargs), self.iables))
+
+    def exists(self):
+        for qs in self.iables:
+            if qs.exists():
+                return True
+        return False
diff --git a/app/organization/media/migrations/0003_auto_20160930_1039.py b/app/organization/media/migrations/0003_auto_20160930_1039.py
new file mode 100644 (file)
index 0000000..0caaa91
--- /dev/null
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-09-30 08:39
+from __future__ import unicode_literals
+
+import datetime
+from django.db import migrations, models
+from django.utils.timezone import utc
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('organization-media', '0002_auto_20160929_1310'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='audio',
+            name='created_at',
+            field=models.DateTimeField(auto_now=True, default=datetime.datetime(2016, 9, 30, 8, 39, 34, 557510, tzinfo=utc)),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='video',
+            name='created_at',
+            field=models.DateTimeField(auto_now=True, default=datetime.datetime(2016, 9, 30, 8, 39, 38, 786940, tzinfo=utc)),
+            preserve_default=False,
+        ),
+    ]
index e7f95411dae12418c8282c983a652f2536d88a02..1cbe00753758b4589c03f8a0de40b67fa2bcc587 100644 (file)
@@ -15,7 +15,6 @@ import requests
 
 MEDIA_BASE_URL = getattr(settings, 'MEDIA_BASE_URL', 'http://medias.ircam.fr/embed/media/')
 
-
 class Media(Titled):
     """Media"""
 
@@ -23,6 +22,7 @@ class Media(Titled):
     open_source_url = models.URLField(_('open source URL'), max_length=1024, blank=True)
     closed_source_url = models.URLField(_('closed source URL'), max_length=1024, blank=True)
     poster_url = models.URLField(_('poster'), max_length=1024, blank=True)
+    created_at = models.DateTimeField(auto_now=True)
 
     class Meta:
         abstract = True
index 4b0faa6ac0a797053fb12bd0d1c3aab0d1bf14fc..9787ed90eae889da2c2750c69c8a98d8f1a78f96 100644 (file)
@@ -11,6 +11,7 @@ from organization.media.views import *
 
 
 urlpatterns = [
+    url(r'^media-list/$', MediaListView.as_view(), name="media"),
     url(r'^videos/$', VideoListView.as_view(), name="festival-video-list"),
     url(r'^videos/detail/(?P<slug>.*)/$', VideoDetailView.as_view(), name="festival-video-detail"),
     url(r'^videos/category/(?P<slug>.*)/$', VideoListCategoryView.as_view(), name="festival-video-list-category"),
index f55156b20ae32ec1f9dcf6b596a6b3d88aadd5d7..cbdce9386372751adcca31b614ab6189e55234ce 100644 (file)
@@ -2,6 +2,7 @@ from django.shortcuts import render
 
 from organization.media.models import *
 from organization.core.views import *
+from organization.core.utils import *
 from dal import autocomplete
 from dal_select2_queryset_sequence.views import Select2QuerySetSequenceView
 from mezzanine_agenda.models import Event
@@ -42,3 +43,19 @@ class VideoListCategoryView(VideoListView):
         context = super(VideoListCategoryView, self).get_context_data(**kwargs)
         context['category'] = self.category
         return context
+
+
+class MediaListView(ListView):
+
+    template_name='media/media_list.html'
+    context_object_name = 'media'
+
+    def get_queryset(self):
+
+        audios = Audio.objects.all()
+        videos = Video.objects.all()
+        qsseq = QuerySetSequence(audios, videos)
+        qsseq.order_by('blog.name','-title')
+        print("----------------------------------")
+        print(len(qsseq))
+        return qsseq
diff --git a/app/templates/media/media_list.html b/app/templates/media/media_list.html
new file mode 100644 (file)
index 0000000..dfa4eff
--- /dev/null
@@ -0,0 +1,36 @@
+{% extends "pages/page.html" %}
+{% load mezzanine_tags keyword_tags i18n organization_tags pages_tags %}
+
+{% block meta_title %}{% trans "Media" %}{% endblock %}
+
+{% block meta_keywords %}{% metablock %}
+{% keywords_for person as keywords %}
+{% for keyword in keywords %}
+    {% if not forloop.first %}, {% endif %}
+    {{ keyword }}
+{% endfor %}
+{% endmetablock %}{% endblock %}
+
+{% block page_class %}
+    person
+{% endblock %}
+
+{% block breadcrumb_menu %}
+    {{ block.super }}
+    <li class="breadcrumb__item active">{% trans "Media" %}</li>
+{% endblock %}
+
+{% block page_title %}
+    {% editable person.title %}
+        <h1 class="dotted">{% trans "Media" %}</h1>
+    {% endeditable %}
+{% endblock %}
+
+{% block page_content %}
+
+{% for m in media %}
+    {{ m.get_content_model }} : {{ m.created_at }} : {{ m.title }}
+    <br>
+{% endfor %}
+
+{% endblock %}