From d6c73e9e07fde404680bca9a6fd4f26db435f205 Mon Sep 17 00:00:00 2001 From: Emilie Date: Fri, 30 Sep 2016 12:40:23 +0200 Subject: [PATCH] Media [wip]: list audio video | issue with order by --- app/organization/core/utils.py | 237 ++++++++++++++++++ .../migrations/0003_auto_20160930_1039.py | 29 +++ app/organization/media/models.py | 2 +- app/organization/media/urls.py | 1 + app/organization/media/views.py | 17 ++ app/templates/media/media_list.html | 36 +++ 6 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 app/organization/core/utils.py create mode 100644 app/organization/media/migrations/0003_auto_20160930_1039.py create mode 100644 app/templates/media/media_list.html diff --git a/app/organization/core/utils.py b/app/organization/core/utils.py new file mode 100644 index 00000000..5c2c918f --- /dev/null +++ b/app/organization/core/utils.py @@ -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 index 00000000..0caaa913 --- /dev/null +++ b/app/organization/media/migrations/0003_auto_20160930_1039.py @@ -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, + ), + ] diff --git a/app/organization/media/models.py b/app/organization/media/models.py index e7f95411..1cbe0075 100644 --- a/app/organization/media/models.py +++ b/app/organization/media/models.py @@ -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 diff --git a/app/organization/media/urls.py b/app/organization/media/urls.py index 4b0faa6a..9787ed90 100644 --- a/app/organization/media/urls.py +++ b/app/organization/media/urls.py @@ -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.*)/$', VideoDetailView.as_view(), name="festival-video-detail"), url(r'^videos/category/(?P.*)/$', VideoListCategoryView.as_view(), name="festival-video-list-category"), diff --git a/app/organization/media/views.py b/app/organization/media/views.py index f55156b2..cbdce938 100644 --- a/app/organization/media/views.py +++ b/app/organization/media/views.py @@ -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 index 00000000..dfa4eff1 --- /dev/null +++ b/app/templates/media/media_list.html @@ -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 }} + +{% endblock %} + +{% block page_title %} + {% editable person.title %} +

{% trans "Media" %}

+ {% endeditable %} +{% endblock %} + +{% block page_content %} + +{% for m in media %} + {{ m.get_content_model }} : {{ m.created_at }} : {{ m.title }} +
+{% endfor %} + +{% endblock %} -- 2.39.5