From 7e890833260e5aef8ae217d195309b76fa91c02f Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 14 Feb 2011 09:19:44 +0100 Subject: [PATCH] lesmianator on steroids --- apps/lesmianator/migrations/0001_initial.py | 100 +++++++++++ apps/lesmianator/migrations/__init__.py | 0 apps/lesmianator/models.py | 169 ++++++++++++++++++ apps/lesmianator/urls.py | 14 ++ apps/lesmianator/views.py | 125 +++++++------ lib/librarian | 2 +- wolnelektury/static/css/master.book.css | 10 +- wolnelektury/static/css/master.css | 1 + .../templates/catalogue/book_detail.html | 1 + .../catalogue/tagged_object_list.html | 3 + .../templates/lesmianator/lesmianator.html | 57 ++++++ wolnelektury/templates/lesmianator/poem.html | 48 ++++- wolnelektury/urls.py | 2 +- 13 files changed, 471 insertions(+), 61 deletions(-) create mode 100644 apps/lesmianator/migrations/0001_initial.py create mode 100644 apps/lesmianator/migrations/__init__.py create mode 100644 apps/lesmianator/models.py create mode 100644 apps/lesmianator/urls.py create mode 100644 wolnelektury/templates/lesmianator/lesmianator.html diff --git a/apps/lesmianator/migrations/0001_initial.py b/apps/lesmianator/migrations/0001_initial.py new file mode 100644 index 000000000..9efb41f5a --- /dev/null +++ b/apps/lesmianator/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'Poem' + db.create_table('lesmianator_poem', ( + ('view_count', self.gf('django.db.models.fields.IntegerField')(default=1)), + ('text', self.gf('django.db.models.fields.TextField')()), + ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('created_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=120, db_index=True)), + ('created_from', self.gf('catalogue.fields.JSONField')(null=True, blank=True)), + ('seen_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + )) + db.send_create_signal('lesmianator', ['Poem']) + + # Adding model 'Continuations' + db.create_table('lesmianator_continuations', ( + ('pickle', self.gf('django.db.models.fields.files.FileField')(max_length=100)), + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + )) + db.send_create_signal('lesmianator', ['Continuations']) + + + def backwards(self, orm): + + # Deleting model 'Poem' + db.delete_table('lesmianator_poem') + + # Deleting model 'Continuations' + db.delete_table('lesmianator_continuations') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'lesmianator.continuations': { + 'Meta': {'object_name': 'Continuations'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'pickle': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}) + }, + 'lesmianator.poem': { + 'Meta': {'object_name': 'Poem'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'created_from': ('catalogue.fields.JSONField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'seen_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '120', 'db_index': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {}), + 'view_count': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + } + } + + complete_apps = ['lesmianator'] diff --git a/apps/lesmianator/migrations/__init__.py b/apps/lesmianator/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/lesmianator/models.py b/apps/lesmianator/models.py new file mode 100644 index 000000000..dce4b15b1 --- /dev/null +++ b/apps/lesmianator/models.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +import cPickle +from datetime import datetime +from random import randint +from StringIO import StringIO + +from django.core.files.base import ContentFile +from django.db import models +from django.db.models import permalink +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse +from django.db.models.signals import m2m_changed +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.conf import settings + +from librarian import text +from catalogue.fields import JSONField +from catalogue.models import Book, Tag + + +class Poem(models.Model): + slug = models.SlugField(_('slug'), max_length=120, db_index=True) + text = models.TextField(_('text')) + created_by = models.ForeignKey(User) + created_from = JSONField(_('extra information'), null=True, blank=True) + created_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False) + seen_at = models.DateTimeField(_('last view date'), auto_now_add=True, editable=False) + view_count = models.IntegerField(_('view count'), default=1) + + try: + f = open(settings.LESMIANATOR_PICKLE) + global_dictionary = cPickle.load(f) + f.close() + except: + global_dictionary = {} + + def visit(self): + self.view_count += 1 + self.seen_at = datetime.now() + self.save() + + def __unicode__(self): + return "%s (%s...)" % (self.slug, self.text[:20]) + + @classmethod + def write(cls, continuations=None, length=3, maxlen=1000): + def choose_word(word, continuations): + try: + choices = sum((continuations[word][post] for post in continuations[word])) + r = randint(0, choices - 1) + + for post in continuations[word]: + r -= continuations[word][post] + if r < 0: + return post + except KeyError: + return '' + + + if continuations is None: + continuations = cls.global_dictionary + + letters = [] + word = u'' + empty = -10 + lines = 0 + if not continuations: + maxlen = 0 + # want at least two lines, but let Lesmianator end his stanzas + while (empty < 2 or lines < 2) and maxlen: + letter = choose_word(word, continuations) + letters.append(letter) + word = word[-length+1:] + letter + if letter == u'\n': + # count non-empty lines + if empty == 0: + lines += 1 + # + if lines >= 2: + empty += 1 + lines += 1 + else: + empty = 0 + maxlen -= 1 + + return ''.join(letters).strip() + + def get_absolute_url(self): + return reverse('get_poem', kwargs={'poem': self.slug}) + + +class Continuations(models.Model): + pickle = models.FileField(_('Continuations file'), upload_to='lesmianator') + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + + def __unicode__(self): + return "Continuations for: %s" % unicode(self.content_object) + + @staticmethod + def join_conts(a, b): + for pre in b: + a.setdefault(pre, {}) + for post in b[pre]: + a[pre].setdefault(post, 0) + a[pre][post] += b[pre][post] + return a + + @classmethod + def for_book(cls, book, length=3): + # count from this book only + print 'for_book', book + output = StringIO() + f = open(book.xml_file.path) + text.transform(f, output, False, ('raw-text',)) + f.close() + conts = {} + last_word = '' + for letter in output.getvalue().decode('utf-8').strip().lower(): + mydict = conts.setdefault(last_word, {}) + mydict.setdefault(letter, 0) + mydict[letter] += 1 + last_word = last_word[-length+1:] + letter + # add children + return reduce(cls.join_conts, + (cls.get(child) for child in book.children.all()), + conts) + + @classmethod + def for_set(cls, tag): + # book contains its descendants, we don't want them twice + books = Book.tagged.with_any((tag,)) + l_tags = Tag.objects.filter(category='book', slug__in=[book.book_tag_slug() for book in books]) + descendants_keys = [book.pk for book in Book.tagged.with_any(l_tags)] + if descendants_keys: + books = books.exclude(pk__in=descendants_keys) + + cont_tabs = (cls.get(b) for b in books) + return reduce(cls.join_conts, cont_tabs) + + @classmethod + def get(cls, sth): + object_type = ContentType.objects.get_for_model(sth) + try: + obj = cls.objects.get(content_type=object_type, object_id=sth.id) + f = open(obj.pickle.path) + conts = cPickle.load(f) + f.close() + return conts + except cls.DoesNotExist: + if isinstance(sth, Book): + conts = cls.for_book(sth) + elif isinstance(sth, Tag): + conts = cls.for_set(sth) + else: + raise NotImplemented('Lesmianator continuations: only Book and Tag supported') + + c = cls(content_object=sth) + c.pickle.save(sth.slug+'.p', ContentFile(cPickle.dumps(conts))) + c.save() + return conts + + diff --git a/apps/lesmianator/urls.py b/apps/lesmianator/urls.py new file mode 100644 index 000000000..e7bbb48d4 --- /dev/null +++ b/apps/lesmianator/urls.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. +# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. +# +from django.conf.urls.defaults import * + +urlpatterns = patterns('lesmianator.views', + url(r'^$', 'main_page', name='lesmianator'), + url(r'^wiersz/$', 'new_poem', name='new_poem'), + url(r'^lektura/(?P[a-zA-Z0-9-]+)/$', 'poem_from_book', name='poem_from_book'), + url(r'^polka/(?P[a-zA-Z0-9-]+)/$', 'poem_from_set', name='poem_from_set'), + url(r'^wiersz/(?P[a-zA-Z0-9-]+)/$', 'get_poem', name='get_poem'), +) + diff --git a/apps/lesmianator/views.py b/apps/lesmianator/views.py index 2d6d53fc5..3307a2598 100644 --- a/apps/lesmianator/views.py +++ b/apps/lesmianator/views.py @@ -1,60 +1,77 @@ # Create your views here. -import cPickle -from django.shortcuts import render_to_response +from django.shortcuts import render_to_response, get_object_or_404 from django.template import RequestContext -from random import randint - - -def _choose_word(word): - try: - choices = sum((_dictionary[word][post] for post in _dictionary[word])) - r = randint(0, choices - 1) - - for post in _dictionary[word]: - r -= _dictionary[word][post] - if r < 0: - return post - except KeyError: - return '' - -# load dictionary on start, it won't change -from django.conf import settings - -try: - f = open(settings.LESMIANATOR_PICKLE) - _dictionary = cPickle.load(f) -except: - _dictionary = {} - - -def poem(request): - letters = [] - word = u'' - empty = -10 - left = 1000 - lines = 0 - if not _dictionary: - left = 0 - # want at least two lines, but let Lesmianator end his stanzas - while (empty < 2 or lines < 2) and left: - letter = _choose_word(word) - letters.append(letter) - word = word[-2:] + letter - if letter == u'\n': - # count non-empty lines - if empty == 0: - lines += 1 - # - if lines >= 2: - empty += 1 - lines += 1 - else: - empty = 0 - left -= 1 - - poem = ''.join(letters).strip() +from django.contrib.auth.decorators import login_required +from django.views.decorators import cache + +from catalogue.utils import get_random_hash +from catalogue.models import Book, Tag +from catalogue import forms +from lesmianator.models import Poem, Continuations + + +def main_page(request): + last = Poem.objects.all().order_by('-created_at')[:10] + form = forms.SearchForm() + shelves = Tag.objects.filter(user__username='lesmianator') + + return render_to_response('lesmianator/lesmianator.html', + {"last": last, "form": form, "shelves": shelves}, + context_instance=RequestContext(request)) + + +@cache.never_cache +def new_poem(request): + user = request.user if request.user.is_authenticated else None + text = Poem.write() + p = Poem(slug=get_random_hash(text), text=text, created_by=user) + p.save() + + return render_to_response('lesmianator/poem.html', + {"poem": p}, + context_instance=RequestContext(request)) + + +@cache.never_cache +def poem_from_book(request, slug): + book = get_object_or_404(Book, slug=slug) + user = request.user if request.user.is_authenticated else None + text = Poem.write(Continuations.get(book)) + p = Poem(slug=get_random_hash(text), text=text, created_by=user) + p.set_created_from_value([book.id]) + p.save() return render_to_response('lesmianator/poem.html', - {"object": poem}, + {"poem": p, "books": [book], "book": book}, context_instance=RequestContext(request)) + + +@cache.never_cache +@login_required +def poem_from_set(request, shelf): + user = request.user + tag = get_object_or_404(Tag, category='set', slug=shelf) + text = Poem.write(Continuations.get(tag)) + p = Poem(slug=get_random_hash(text), text=text, created_by=user) + books = Book.tagged.with_any((tag,)) + p.set_created_from_value([b.id for b in books]) + p.save() + + book = books[0] if len(books) == 1 else None + + return render_to_response('lesmianator/poem.html', + {"poem": p, "shelf": tag, "books": books, "book": book}, + context_instance=RequestContext(request)) + +def get_poem(request, poem): + p = get_object_or_404(Poem, slug=poem) + p.visit() + books = Book.objects.filter(id__in=p.get_created_from_value()) + book = books[0] if len(books) == 1 else None + + return render_to_response('lesmianator/poem.html', + {"poem": p, "books": books, "book": book}, + context_instance=RequestContext(request)) + + diff --git a/lib/librarian b/lib/librarian index c69605017..f492e325e 160000 --- a/lib/librarian +++ b/lib/librarian @@ -1 +1 @@ -Subproject commit c69605017947202b0bbd0a2a2335cb37752141b9 +Subproject commit f492e325efb42a3348b2479a0fd0ffc3c484657b diff --git a/wolnelektury/static/css/master.book.css b/wolnelektury/static/css/master.book.css index f18f15729..83bad314e 100644 --- a/wolnelektury/static/css/master.book.css +++ b/wolnelektury/static/css/master.book.css @@ -203,6 +203,10 @@ p { top: -4px; } +#footnotes { + margin-top: 3em; +} + #footnotes .annotation { display: block; float: left; @@ -214,11 +218,15 @@ p { margin: 1.5em 0 0 0; } -#footnotes p { +#footnotes p, #footnotes ul { margin-left: 2.5em; font-size: 0.875em; } +#footnotes .permalink { + font-size: .75em; +} + blockquote { font-size: 0.875em; } diff --git a/wolnelektury/static/css/master.css b/wolnelektury/static/css/master.css index fa8ddc189..459c68279 100644 --- a/wolnelektury/static/css/master.css +++ b/wolnelektury/static/css/master.css @@ -280,6 +280,7 @@ p .ac_input { border-right: 0.15em solid #E3D888; } +#lesmianator #tags-list, #tagged-object-list #tags-list, #book-detail #tags-list { margin-left: 39em; } diff --git a/wolnelektury/templates/catalogue/book_detail.html b/wolnelektury/templates/catalogue/book_detail.html index 11edda829..660bf2602 100644 --- a/wolnelektury/templates/catalogue/book_detail.html +++ b/wolnelektury/templates/catalogue/book_detail.html @@ -163,6 +163,7 @@ {% endif %}

{% trans "View XML source" %}

+

Miksuj ten utwór

{% trans "Work's themes " %}

diff --git a/wolnelektury/templates/catalogue/tagged_object_list.html b/wolnelektury/templates/catalogue/tagged_object_list.html index 59ba5e5fa..54db1d0fe 100644 --- a/wolnelektury/templates/catalogue/tagged_object_list.html +++ b/wolnelektury/templates/catalogue/tagged_object_list.html @@ -124,6 +124,9 @@

{% trans "Epochs" %}

{% tag_list categories.epoch tags %} {% endif %} + {% if only_shelf %} +

Miksuj utwory z tej półki + {% endif %}

{% if categories.theme %} diff --git a/wolnelektury/templates/lesmianator/lesmianator.html b/wolnelektury/templates/lesmianator/lesmianator.html new file mode 100644 index 000000000..8f488e8c5 --- /dev/null +++ b/wolnelektury/templates/lesmianator/lesmianator.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} +{% load i18n %} +{% load catalogue_tags %} + +{% block title %}Leśmianator w WolneLektury.pl{% endblock %} + +{% block bodyid %}lesmianator{% endblock %} + +{% block body %} +

Leśmianator

+
+

{{ form.q }} {% trans "or" %} {% trans "return to main page" %}

+
+ +
+

Leśmianator tworzy wierszmiksy – dzięki niemu + napiszesz wiersz jednym kliknięciem. + W nowej odłonie nowe możliwości zabawy – teraz możesz zdecydować, co wrzucasz do miksera, + a swoimi dziełami podzielić się z przyjaciółmi!

+

Przygotowaliśmy kilka propozycji na start – możesz wybrać jedną z nich, + albo ułożyć sobie własną, niepowtarzalną mieszankę.

+
+ +
+

Miksuj utwory

+

Możesz zmiksować całą lirykę w naszej bibliotece + albo tylko jeden konkretny utwór. Jak? Wejdź na + stronę utworu, + kliknij w link „miksuj ten utwór” – i gotowe!

+ +

Miksuj półki

+

Załóż konto, poukładaj swoje ulubione książki na półkach i miksuj + w dowolnych konfiguracjach. Nic prostszego, niż zmieszać + Leśmiana z + Tetmajerem, + a Żeromskiego z + Sienkiewiczem + – wystarczy wrzucić ich utwory na półkę i kliknąć + w link „miksuj utwory z tej półki”.

+ +

Dziel się z innymi

+

Wyszedł Ci wyjątkowo udany wierszmiks? Znajdziesz pod nim link, przy pomocy + którego możesz się nim podzielić z przyjaciółmi.

+ +

Miłej zabawy!

+ +
+{% endblock %} \ No newline at end of file diff --git a/wolnelektury/templates/lesmianator/poem.html b/wolnelektury/templates/lesmianator/poem.html index 1f834650e..1dbf89218 100644 --- a/wolnelektury/templates/lesmianator/poem.html +++ b/wolnelektury/templates/lesmianator/poem.html @@ -1,5 +1,5 @@ {% load i18n %} -{% load chunks compressed catalogue_tags %} +{% load compressed %} @@ -11,8 +11,13 @@