From 8830e06ad0b466c40747540b5122e6825114a90a Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 15 Jun 2011 16:12:13 +0200 Subject: [PATCH 1/1] assigning tickets, new ui --- ...document_user__add_field_document_stage.py | 96 ++++++++++++++++++ apps/dvcs/models.py | 9 +- apps/wiki/forms.py | 5 +- apps/wiki/helpers.py | 14 +++ apps/wiki/models.py | 5 + apps/wiki/templates/wiki/base.html | 47 +++++++-- apps/wiki/templates/wiki/book_detail.html | 3 + apps/wiki/templates/wiki/chunk_list_item.html | 9 ++ apps/wiki/templates/wiki/document_list.html | 5 +- apps/wiki/templates/wiki/main_tabs.html | 3 + apps/wiki/templatetags/wiki.py | 38 +++++-- apps/wiki/urls.py | 5 + apps/wiki/views.py | 38 ++++++- redakcja/settings/common.py | 2 + redakcja/static/css/filelist.css | 62 +++++++---- redakcja/static/img/wl-orange.png | Bin 0 -> 1371 bytes requirements.txt | 1 + 17 files changed, 301 insertions(+), 41 deletions(-) create mode 100644 apps/dvcs/migrations/0002_auto__add_field_document_user__add_field_document_stage.py create mode 100755 apps/wiki/templates/wiki/chunk_list_item.html create mode 100755 apps/wiki/templates/wiki/main_tabs.html create mode 100644 redakcja/static/img/wl-orange.png diff --git a/apps/dvcs/migrations/0002_auto__add_field_document_user__add_field_document_stage.py b/apps/dvcs/migrations/0002_auto__add_field_document_user__add_field_document_stage.py new file mode 100644 index 00000000..41ab9b24 --- /dev/null +++ b/apps/dvcs/migrations/0002_auto__add_field_document_user__add_field_document_stage.py @@ -0,0 +1,96 @@ +# 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 field 'Document.user' + db.add_column('dvcs_document', 'user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True), keep_default=False) + + # Adding field 'Document.stage' + db.add_column('dvcs_document', 'stage', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['dvcs.Tag'], null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'Document.user' + db.delete_column('dvcs_document', 'user_id') + + # Deleting field 'Document.stage' + db.delete_column('dvcs_document', 'stage_id') + + + 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': {'ordering': "('content_type__app_label', 'codename')", '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'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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': {'ordering': "('name',)", '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'}) + }, + 'dvcs.change': { + 'Meta': {'ordering': "('created_at',)", 'unique_together': "(['tree', 'revision'],)", 'object_name': 'Change'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'author_desc': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'merge_parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'merge_children'", 'null': 'True', 'blank': 'True', 'to': "orm['dvcs.Change']"}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'blank': 'True', 'to': "orm['dvcs.Change']"}), + 'patch': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'publishable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'revision': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['dvcs.Tag']", 'symmetrical': 'False'}), + 'tree': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Document']"}) + }, + 'dvcs.document': { + 'Meta': {'object_name': 'Document'}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'created_documents'", 'null': 'True', 'to': "orm['auth.User']"}), + 'head': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['dvcs.Change']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'stage': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['dvcs.Tag']", 'null': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}) + }, + 'dvcs.tag': { + 'Meta': {'ordering': "['ordering']", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'ordering': ('django.db.models.fields.IntegerField', [], {}), + 'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['dvcs'] diff --git a/apps/dvcs/models.py b/apps/dvcs/models.py index 262472a3..a72556f6 100644 --- a/apps/dvcs/models.py +++ b/apps/dvcs/models.py @@ -38,6 +38,9 @@ class Tag(models.Model): def listener_changed(sender, instance, **kwargs): sender._object_cache = {} + def next(self): + Tag.objects.filter(ordering__gt=self.ordering) + models.signals.pre_save.connect(Tag.listener_changed, sender=Tag) @@ -168,12 +171,16 @@ class Document(models.Model): """ File in repository. """ - creator = models.ForeignKey(User, null=True, blank=True, editable=False) + creator = models.ForeignKey(User, null=True, blank=True, editable=False, + related_name="created_documents") head = models.ForeignKey(Change, null=True, blank=True, default=None, help_text=_("This document's current head."), editable=False) + user = models.ForeignKey(User, null=True, blank=True) + stage = models.ForeignKey(Tag, null=True, blank=True) + def __unicode__(self): return u"{0}, HEAD: {1}".format(self.id, self.head_id) diff --git a/apps/wiki/forms.py b/apps/wiki/forms.py index 55b1e544..3626b6d3 100644 --- a/apps/wiki/forms.py +++ b/apps/wiki/forms.py @@ -40,7 +40,8 @@ class DocumentCreateForm(forms.ModelForm): class Meta: model = Book - exclude = ['gallery'] + exclude = ['gallery', 'parent', 'parent_number'] + prepopulated_fields = {'slug': ['title']} def clean(self): super(DocumentCreateForm, self).clean() @@ -164,7 +165,7 @@ class ChunkForm(forms.ModelForm): chunk = Chunk.objects.get(book=self.instance.book, slug=slug) except Chunk.DoesNotExist: return slug - if chunk == self: + if chunk == self.instance: return slug raise forms.ValidationError(_('Chunk with this slug already exists')) diff --git a/apps/wiki/helpers.py b/apps/wiki/helpers.py index f072ef91..ee26c5a2 100644 --- a/apps/wiki/helpers.py +++ b/apps/wiki/helpers.py @@ -129,3 +129,17 @@ def recursive_groupby(iterable): grouper = None return list(_generator(iterable)) + + +def active_tab(tab): + """ + View decorator, which puts tab info on a request. + """ + def wrapper(f): + @wraps(f) + def wrapped(request, *args, **kwargs): + request.wiki_active_tab = tab + return f(request, *args, **kwargs) + return wrapped + return wrapper + diff --git a/apps/wiki/models.py b/apps/wiki/models.py index 7887e5da..9eb77a5c 100644 --- a/apps/wiki/models.py +++ b/apps/wiki/models.py @@ -188,6 +188,11 @@ class Chunk(dvcs_models.Document): creator=creator, slug=slug, comment=comment) return new_chunk + def list_html(self): + _list_html = render_to_string('wiki/chunk_list_item.html', + {'chunk': self}) + return mark_safe(_list_html) + @staticmethod def listener_saved(sender, instance, created, **kwargs): if instance.book: diff --git a/apps/wiki/templates/wiki/base.html b/apps/wiki/templates/wiki/base.html index f88fac31..85abaaf4 100644 --- a/apps/wiki/templates/wiki/base.html +++ b/apps/wiki/templates/wiki/base.html @@ -1,18 +1,36 @@ -{% extends "base.html" %} {% load compressed i18n %} +{% load wiki %} + + + + + {% compressed_css 'listing' %} + + {% block title %}{% trans "Platforma Redakcyjna" %}{% endblock title %} + + -{% block title %}{{ document_name }} - {{ block.super }}{% endblock %} +
-{% block extrahead %} -{% compressed_css 'listing' %} -{% endblock %} + -{% block extrabody %} -{% compressed_js 'listing' %} -{% endblock %} +
+ {% main_tabs %} + +
+ + + {% include "registration/head_login.html" %} + + +
+
+ +
-{% block maincontent %} -

{% trans "Platforma Redakcyjna" %}

{% block leftcolumn %} {% endblock leftcolumn %} @@ -21,4 +39,11 @@ {% block rightcolumn %} {% endblock rightcolumn %}
-{% endblock maincontent %} \ No newline at end of file + +
+ +{% compressed_js 'listing' %} +{% block extrabody %} +{% endblock %} + + diff --git a/apps/wiki/templates/wiki/book_detail.html b/apps/wiki/templates/wiki/book_detail.html index f4b15a9e..347b7a37 100755 --- a/apps/wiki/templates/wiki/book_detail.html +++ b/apps/wiki/templates/wiki/book_detail.html @@ -45,6 +45,9 @@ [{% trans "edit" %}] {% if c.chunk.publishable %}P{% endif %} + {% if c.chunk.user.is_authenticated %} + {{ c.chunk.user }} + {% endif %} [+] {% endfor %} diff --git a/apps/wiki/templates/wiki/chunk_list_item.html b/apps/wiki/templates/wiki/chunk_list_item.html new file mode 100755 index 00000000..bec9e75a --- /dev/null +++ b/apps/wiki/templates/wiki/chunk_list_item.html @@ -0,0 +1,9 @@ + + + [?] + + {{ chunk.pretty_name }} + + diff --git a/apps/wiki/templates/wiki/document_list.html b/apps/wiki/templates/wiki/document_list.html index 25b4cf27..221b63d2 100644 --- a/apps/wiki/templates/wiki/document_list.html +++ b/apps/wiki/templates/wiki/document_list.html @@ -1,6 +1,7 @@ {% extends "wiki/base.html" %} {% load i18n %} +{% load pagination_tags %} {% block extrabody %} {{ block.super }} @@ -22,7 +23,7 @@ $(function() { {% block leftcolumn %}
- +
@@ -30,10 +31,12 @@ $(function() { + {% autopaginate books 20 %} {% for book in books %} {{ book.list_html }} {% endfor %} +
Filtr:
{% paginate %}
{% endblock leftcolumn %} diff --git a/apps/wiki/templates/wiki/main_tabs.html b/apps/wiki/templates/wiki/main_tabs.html new file mode 100755 index 00000000..82321cc4 --- /dev/null +++ b/apps/wiki/templates/wiki/main_tabs.html @@ -0,0 +1,3 @@ +{% for tab in tabs %} + {{ tab.caption }} +{% endfor %} diff --git a/apps/wiki/templatetags/wiki.py b/apps/wiki/templatetags/wiki.py index cb5bf20c..8acf7619 100644 --- a/apps/wiki/templatetags/wiki.py +++ b/apps/wiki/templatetags/wiki.py @@ -1,14 +1,40 @@ from __future__ import absolute_import +from django.core.urlresolvers import reverse from django.template.defaultfilters import stringfilter from django import template +from django.utils.translation import ugettext as _ + register = template.Library() -from wiki.models import split_name -@register.filter -@stringfilter -def wiki_title(value): - parts = (p.replace('_', ' ').title() for p in split_name(value)) - return ' / '.join(parts) +class Tab(object): + slug = None + caption = None + url = None + + def __init__(self, slug, caption, url): + self.slug = slug + self.caption = caption + self.url = url + + +@register.inclusion_tag("wiki/main_tabs.html", takes_context=True) +def main_tabs(context): + active = getattr(context['request'], 'wiki_active_tab', None) + + tabs = [] + user = context['user'] + if user.is_authenticated(): + tabs.append(Tab('my', _('Assigned to me'), reverse("wiki_user"))) + + tabs.append(Tab('unassigned', _('Unassigned'), reverse("wiki_unassigned"))) + tabs.append(Tab('all', _('All'), reverse("wiki_document_list"))) + tabs.append(Tab('create', _('Add'), reverse("wiki_create_missing"))) + tabs.append(Tab('upload', _('Upload'), reverse("wiki_upload"))) + + if user.is_staff: + tabs.append(Tab('admin', _('Admin'), reverse("admin:index"))) + + return {"tabs": tabs, "active_tab": active} diff --git a/apps/wiki/urls.py b/apps/wiki/urls.py index c7da6ef8..f0b602c7 100644 --- a/apps/wiki/urls.py +++ b/apps/wiki/urls.py @@ -15,6 +15,9 @@ urlpatterns = patterns('wiki.views', #url(r'^catalogue/([^/]+)/$', 'document_list'), #url(r'^catalogue/([^/]+)/([^/]+)/$', 'document_list'), #url(r'^catalogue/([^/]+)/([^/]+)/([^/]+)$', 'document_list'), + url(r'^unassigned/$', 'unassigned', name='wiki_unassigned'), + url(r'^user/$', 'my', name='wiki_user'), + url(r'^user/(?P[^/]+)/$', 'user', name='wiki_user'), url(r'^edit/(?P[^/]+)/(?:(?P[^/]+)/)?$', 'editor', name="wiki_editor"), @@ -27,6 +30,8 @@ urlpatterns = patterns('wiki.views', url(r'^create/(?P[^/]*)/', 'create_missing', name='wiki_create_missing'), + url(r'^create/', + 'create_missing', name='wiki_create_missing'), url(r'^gallery/(?P[^/]+)/$', 'gallery', name="wiki_gallery"), diff --git a/apps/wiki/views.py b/apps/wiki/views.py index a68fa35d..0821488f 100644 --- a/apps/wiki/views.py +++ b/apps/wiki/views.py @@ -7,12 +7,13 @@ from lxml import etree from django.conf import settings +from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.views.generic.simple import direct_to_template from django.views.decorators.http import require_POST, require_GET from django.core.urlresolvers import reverse from wiki.helpers import (JSONResponse, JSONFormInvalid, JSONServerError, - ajax_require_permission, recursive_groupby) + ajax_require_permission, recursive_groupby, active_tab) from django import http from django.shortcuts import get_object_or_404, redirect from django.http import Http404 @@ -41,6 +42,7 @@ import operator MAX_LAST_DOCS = 10 +@active_tab('all') @never_cache def document_list(request): return direct_to_template(request, 'wiki/document_list.html', extra_context={ @@ -50,6 +52,34 @@ def document_list(request): }) +@active_tab('unassigned') +@never_cache +def unassigned(request): + return direct_to_template(request, 'wiki/document_list.html', extra_context={ + 'books': Chunk.objects.filter(user=None), + 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), + key=lambda x: x[1]['time'], reverse=True), + }) + + +@never_cache +def user(request, username=None): + if username is None: + if request.user.is_authenticated(): + user = request.user + else: + raise Http404 + else: + user = get_object_or_404(User, username=username) + + return direct_to_template(request, 'wiki/document_list.html', extra_context={ + 'books': Chunk.objects.filter(user=user), + 'last_books': sorted(request.session.get("wiki_last_books", {}).items(), + key=lambda x: x[1]['time'], reverse=True), + }) +my = login_required(active_tab('my')(user)) + + @never_cache def editor(request, slug, chunk=None, template_name='wiki/document_details.html'): try: @@ -118,7 +148,10 @@ def editor_readonly(request, slug, chunk=None, template_name='wiki/document_deta }) -def create_missing(request, slug): +@active_tab('create') +def create_missing(request, slug=None): + if slug is None: + slug = '' slug = slug.replace(' ', '-') if request.method == "POST": @@ -148,6 +181,7 @@ def create_missing(request, slug): }) +@active_tab('upload') def upload(request): if request.method == "POST": form = forms.DocumentsUploadForm(request.POST, request.FILES) diff --git a/redakcja/settings/common.py b/redakcja/settings/common.py index f5e90de3..36c54b00 100644 --- a/redakcja/settings/common.py +++ b/redakcja/settings/common.py @@ -81,6 +81,7 @@ MIDDLEWARE_CLASSES = ( 'django_cas.middleware.CASMiddleware', 'django.middleware.doc.XViewMiddleware', + 'pagination.middleware.PaginationMiddleware', 'maintenancemode.middleware.MaintenanceModeMiddleware', ) @@ -117,6 +118,7 @@ INSTALLED_APPS = ( 'south', 'sorl.thumbnail', 'filebrowser', + 'pagination', 'dvcs', 'wiki', diff --git a/redakcja/static/css/filelist.css b/redakcja/static/css/filelist.css index f6f55d7a..bb6bea7f 100644 --- a/redakcja/static/css/filelist.css +++ b/redakcja/static/css/filelist.css @@ -7,28 +7,57 @@ */ body { - background-color: #84BF2A; + margin: 0; + font-family: verdana, sans-serif; + font-size: 12px; } -#content { - background: #EFEFEF; - border: 1px solid black; - padding: 0.5em 2em; - margin: 1em; - overflow: hidden; + +.clr { + clear: both; +} + +#tabs-nav { + padding: 5px 5px 0 10px; + background: #ffdfbf; + border-bottom: 1px solid #ff8000; + position: relative; +} + +#tabs-nav-left { + margin-left: 60px; +} + +#tabs-nav-left a { + display: block; + float: left; + padding: 5px 20px 5px 20px; + margin-bottom: -1px; + border-width: 1px; + border-style: solid; + border-color: rgba(0,0,0,0); } -#content h1 img { - vertical-align: middle; +#tabs-nav-left .active { + background: white; + border-color: #ff8000 #ff8000 white #ff8000; } -#content h1 { - border-bottom: 2px solid black; - padding: 0.5em; - font-size: 2opt; - font-family: sans-serif; +#login-box { + float: right; } +#logo { + position: absolute; + bottom: 0; +} + +#content { + padding: 10px; +} + + + #file-list { overflow: visible; float: left; @@ -58,7 +87,7 @@ body { } a, a:visited, a:active { - color: blue; + color: #bf6000; text-decoration: none; } @@ -67,9 +96,6 @@ a:hover { } -#loading-overlay { - display: none; -} .error { color: red; diff --git a/redakcja/static/img/wl-orange.png b/redakcja/static/img/wl-orange.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c56d059608b53e4524e3a9ea4442ac2a81da82 GIT binary patch literal 1371 zcmV-h1*H0kP)n+a8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11lUPLK~z|Uz1Ulf-BlIG@y|L_N6S=VD+yL2RwN*ZRHY4IA)RRx z)G!t=sL@hlOsJQLjj^d|Xy?y=hRY}zk@7^)2qCn!V*zW0gn+g*l#pO+two4pk!lm_ zgBGSxXwUNDe=@x=moo>aJJ~O1?e$yVz4rRAy>lWno$yh{d-0$!peKLWs!Z+wh}7J; zG2Vwwm?bTKD^e#GzKj_T*^D;afHRbFZmV)94!(>tR2e^PlDXR5j>G6~Rr18Z9}P~Y z>+(g*b04J$F zbKIlI+s9b_C_bc&_l!5p#K4CO&|$LgG_m@zhR?W5r#1L8YRZ^4is}w<(2Oy)!r7=} zWW5o?v^Eywd1ah8to5+J;efgikE+raACZ2Ar=8>%$%IH(ZQ>yIVk1u08>gpa-MyDco zX)_zI;0a}P9xG+LkutyE&u})nt;oOYjccTP@q3dkZ3Y}7uh%YgNmpP2PQz<>TzEu! zMB$HQ>W`@o+K7+gD~f!;tx&E3 zZN_>mQsklWz;DAx@v`a-vR;RWEX~1TJ<2!<1FD;Ii6ZZ4{I(HugoS!?ojc=g_S%mN z6}h9~`>Dpd?egs~G(p|V(Rm%fSEA+)& zbp!Y>uEbyQJG84=tnLHYgU2x&FR4mp3NBUTUs?fYsM}=X8mz%;Wn9^`!phual392W zTk&0C4bE4$SCMN}-^XqoQk6H%1J~v@s1F)KP&mObUBQY*hmGW*?N7{3= zd1QF#jrr0#>=sr@=NtukIug6^q-e~#Q-zt*T1EcrD3E#hkHWTbp5&RT>`%pK75P+i z@JE~naE|I&w-3$9wpo@hH`cYQdjBE};u%HGZY1FcQh!5ni#Cg`$Y+|~z>ngcUbPVy z;M0oS+#GxbRx@2&=1.1.1,<1.2 sorl-thumbnail>=3.2 django-maintenancemode>=0.9 +django-pagination # migrations south>=0.6 -- 2.20.1