diff --git a/config/requirements.txt b/config/requirements.txt index ad9df07..5cdc8ef 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -20,3 +20,6 @@ psycopg2==2.5.2 # Files django-filer==0.9.5 + +# Blog +django-ckeditor-updated==4.4.0 diff --git a/shelfzilla/apps/blog/__init__.py b/shelfzilla/apps/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shelfzilla/apps/blog/admin.py b/shelfzilla/apps/blog/admin.py new file mode 100644 index 0000000..f29266b --- /dev/null +++ b/shelfzilla/apps/blog/admin.py @@ -0,0 +1,72 @@ +from django.contrib import admin +from .models import Entry, Tag +from django import forms +from django.utils.translation import ugettext as _ +from django import forms +from django.db import models +from ckeditor.widgets import CKEditorWidget +import reversion + + +# +# ENTRY +# +class EntryAdmin(reversion.VersionAdmin): + list_display = ('title', 'date', 'status', 'tag_list', 'preview_link') + list_display_links = ('title', ) + + list_filter = ('date', 'draft', ) + search_fields = ('title', 'content', ) + + prepopulated_fields = {"slug": ("title",)} + + suit_form_tabs = ( + ('general', _('General')), + ('content', _('Content')), + ) + + fieldsets = [ + (None, { + 'classes': ('suit-tab suit-tab-general',), + 'fields': ('title', 'slug', 'draft', 'date', 'tags', ) + }), + (None, { + 'classes': ('suit-tab suit-tab-content full-width',), + 'fields': ('content', ) + }), + ] + + def preview_link(self, obj): + return 'View »' % ( + obj.get_absolute_url() + ) + preview_link.allow_tags = True + + def tag_list(self, obj): + return ", ".join([x.name for x in obj.tags.all()]) + + def save_model(self, request, obj, form, change): + if not change: + obj.author = request.user + super(self.__class__, self).save_model(request, obj, form, change) + + class Media: + #css = { + # "all": ("ckeditor/redactor.css",) + #} + # js = ( + # "ckeditor/ckeditor.js", + # "js/wysiwyg.js", + # ) + pass + +admin.site.register(Entry, EntryAdmin) + + +# +# TAG +# +class TagAdmin(reversion.VersionAdmin): + pass + +admin.site.register(Tag, TagAdmin) diff --git a/shelfzilla/apps/blog/migrations/0001_initial.py b/shelfzilla/apps/blog/migrations/0001_initial.py new file mode 100644 index 0000000..131810b --- /dev/null +++ b/shelfzilla/apps/blog/migrations/0001_initial.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as 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 'Entry' + db.create_table(u'blog_entry', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('title', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(2014, 9, 4, 0, 0))), + ('content', self.gf('ckeditor.fields.RichTextField')()), + ('slug', self.gf('django.db.models.fields.SlugField')(max_length=128)), + ('draft', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('author', self.gf('django.db.models.fields.related.ForeignKey')(related_name='author', to=orm['auth.User'])), + )) + db.send_create_signal('blog', ['Entry']) + + # Adding M2M table for field tags on 'Entry' + m2m_table_name = db.shorten_name(u'blog_entry_tags') + db.create_table(m2m_table_name, ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('entry', models.ForeignKey(orm['blog.entry'], null=False)), + ('tag', models.ForeignKey(orm['blog.tag'], null=False)) + )) + db.create_unique(m2m_table_name, ['entry_id', 'tag_id']) + + # Adding model 'Tag' + db.create_table(u'blog_tag', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('color', self.gf('django.db.models.fields.CharField')(max_length=6, blank=True)), + )) + db.send_create_signal('blog', ['Tag']) + + + def backwards(self, orm): + # Deleting model 'Entry' + db.delete_table(u'blog_entry') + + # Removing M2M table for field tags on 'Entry' + db.delete_table(db.shorten_name(u'blog_entry_tags')) + + # Deleting model 'Tag' + db.delete_table(u'blog_tag') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'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', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'blog.entry': { + 'Meta': {'ordering': "['-date']", 'object_name': 'Entry'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'author'", 'to': u"orm['auth.User']"}), + 'content': ('ckeditor.fields.RichTextField', [], {}), + 'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2014, 9, 4, 0, 0)'}), + 'draft': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['blog.Tag']", 'null': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'blog.tag': { + 'Meta': {'ordering': "['name']", 'object_name': 'Tag'}, + 'color': ('django.db.models.fields.CharField', [], {'max_length': '6', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + u'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'}), + u'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'}) + } + } + + complete_apps = ['blog'] \ No newline at end of file diff --git a/shelfzilla/apps/blog/migrations/__init__.py b/shelfzilla/apps/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shelfzilla/apps/blog/models.py b/shelfzilla/apps/blog/models.py new file mode 100644 index 0000000..067225a --- /dev/null +++ b/shelfzilla/apps/blog/models.py @@ -0,0 +1,71 @@ +from django.db import models +from django.conf import settings +from datetime import datetime +from django.utils.timezone import utc +from ckeditor.fields import RichTextField +from django.core.urlresolvers import reverse +from django.utils.translation import activate + + +# +# ENTRY +# +class Entry(models.Model): + title = models.CharField(max_length=128) + date = models.DateTimeField(default=datetime.now(tz=utc)) + content = RichTextField() + slug = models.SlugField(max_length=128) + draft = models.BooleanField(default=True) + author = models.ForeignKey( + settings.AUTH_USER_MODEL, + editable=False, + related_name='author' + ) + tags = models.ManyToManyField('Tag', null=True, blank=True) + + def __unicode__(self): + return self.title + + def status(self): + status = 'Published' + + if self.date > datetime.now(tz=utc): + status = 'Scheduled' + + if self.draft: + status = 'Draft' + + return status + + def get_absolute_url(self): + kwargs = { + 'year': self.date.year, + 'month': self.date.strftime("%m"), + 'day': self.date.strftime("%d"), + 'slug': self.slug + } + url = reverse('blog:item', kwargs=kwargs) + + return url + + class Meta: + app_label = 'blog' + ordering = ['-date'] + verbose_name_plural = 'Entries' + + +# +# TAG +# +class Tag(models.Model): + name = models.CharField(max_length=128) + color = models.CharField(max_length=6, blank=True) + + def __unicode__(self): + return self.name + + class Meta: + app_label = 'blog' + ordering = ['name'] + + diff --git a/shelfzilla/apps/blog/sitemap.py b/shelfzilla/apps/blog/sitemap.py new file mode 100644 index 0000000..9492d01 --- /dev/null +++ b/shelfzilla/apps/blog/sitemap.py @@ -0,0 +1,16 @@ +from datetime import datetime + +from django.contrib.sitemaps import Sitemap + +from .models import Entry + + +class BlogSitemap(Sitemap): + changefreq = "monthly" + priority = 0.5 + + def items(self): + return Entry.objects.filter(draft=False, date__lte=datetime.now()) + + def lastmod(self, obj): + return obj.date diff --git a/shelfzilla/apps/blog/templatetags/__init__.py b/shelfzilla/apps/blog/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/shelfzilla/apps/blog/templatetags/content.py b/shelfzilla/apps/blog/templatetags/content.py new file mode 100644 index 0000000..11cc795 --- /dev/null +++ b/shelfzilla/apps/blog/templatetags/content.py @@ -0,0 +1,10 @@ +from django_jinja import library + +from fmartingrcom.apps.config.models import SiteConfiguration + + +@library.filter +def readmore(content): + config = SiteConfiguration.objects.get() + summary, rest = content.split(config.readmore_tag, 1) + return summary diff --git a/shelfzilla/apps/blog/templatetags/datetime.py b/shelfzilla/apps/blog/templatetags/datetime.py new file mode 100644 index 0000000..0e9f8a2 --- /dev/null +++ b/shelfzilla/apps/blog/templatetags/datetime.py @@ -0,0 +1,18 @@ +from pytz import timezone + +from django.utils.translation import ugettext as _ +from django.utils.encoding import smart_unicode +from django.conf import settings + +from django_jinja import library + + +@library.filter +def dt(t, fmt=None): + """ + Call ``datetime.strftime`` with the given format string. + """ + tz = timezone(settings.TIME_ZONE) + if fmt is None: + fmt = _('%B %e, %Y') + return smart_unicode(tz.normalize(t).strftime(fmt)) if t else u'' diff --git a/shelfzilla/apps/blog/urls.py b/shelfzilla/apps/blog/urls.py new file mode 100644 index 0000000..40f0e74 --- /dev/null +++ b/shelfzilla/apps/blog/urls.py @@ -0,0 +1,38 @@ +from django.conf.urls import patterns, url + +from .views import ListView, EntryView, SearchView, RSSView + + +urlpatterns = patterns( + None, + # Post list with page + url( + r'^page/(?P\d+)/$', + ListView.as_view(), + name='list' + ), + # Post list + url( + r'^$', + ListView.as_view(), + name='list' + ), + # Single entry + url( + r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w\-]+)/$', + EntryView.as_view(), + name='item' + ), + # RSS + url( + r'^rss\.xml$', + RSSView.as_view(), + name='rss' + ), + # Search + url( + r'^search/$', + SearchView.as_view(), + name='search', + ) +) diff --git a/shelfzilla/apps/blog/utils.py b/shelfzilla/apps/blog/utils.py new file mode 100644 index 0000000..14cc457 --- /dev/null +++ b/shelfzilla/apps/blog/utils.py @@ -0,0 +1,55 @@ +from datetime import datetime + +from django.core.paginator import Paginator +from django.utils import translation +from django.conf import settings +from django.core.paginator import Paginator +from django.db.models import Q + +from .models import Entry + + +class Config(object): + entries_per_page = 10 + +config = Config() + + +def get_posts(query=None, limit=None): + items = Entry.objects.filter( + draft=False, + date__lt=datetime.now() + ) + if query and len(query) > 0: + items = items.filter( + Q(title__icontains=query) | \ + Q(content__icontains=query) | \ + Q(tags__name__iexact=query) + ).distinct() + + items = items.order_by('-date') + + if limit: + items = items[:limit] + + return items + + +def get_paginator(request, page_number=1, item=None, **kwargs): + item_index = None + page = None + items = get_posts(query=kwargs.get('query', None)) + entries_per_page = config.entries_per_page + paginator = Paginator(items, entries_per_page) + if item: + for index, obj in enumerate(items): + if obj == item: + item_index = index + break + if item_index: + page_number = (item_index / entries_per_page) + 1 + + if page_number: + page = paginator.page(page_number) + + return paginator, page diff --git a/shelfzilla/apps/blog/views.py b/shelfzilla/apps/blog/views.py new file mode 100644 index 0000000..819a272 --- /dev/null +++ b/shelfzilla/apps/blog/views.py @@ -0,0 +1,103 @@ +from datetime import datetime + +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.http import Http404, HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.conf import settings + +from shelfzilla.views import View +import utils as blog_utils +from .models import Entry + + +class ListView(View): + section = 'blog' + template = 'blog/list.html' + + def get(self, request, page_number=1): + if 'page' in request.GET: + page_number = int(request.GET['page']) + + paginator, page = blog_utils.get_paginator(request, page_number) + + context = {} + + context['page'] = page + context['page_number'] = page_number + context['paginator'] = paginator + + context = RequestContext(request, context) + return render_to_response(self.template, context_instance=context) + + +class EntryView(View): + section = 'blog' + template = 'blog/entry.jinja' + + def get(self, request, year, month, day, slug): + try: + filters = { + 'slug': slug, + 'date__year': int(year), + 'date__month': int(month), + 'date__day': int(day), + } + + item = Entry.objects.get(**filters) + except Entry.DoesNotExist: + raise Http404 + + paginator, page = blog_utils.get_paginator(request, item=item) + + context = {} + context['page'] = page + context['paginator'] = paginator + context['item'] = item + + context = RequestContext(request, context) + return render_to_response(self.template, context_instance=context) + + +class SearchView(ListView): + template = 'blog/search.jinja' + + def post(self, request): + page_number = 1 + if 'page' in request.GET: + page_number = int(request.GET['page']) + + search_query = request.POST['query'] + + if not search_query: + return HttpResponseRedirect(reverse('blog:list')) + + paginator, page = blog_utils.get_paginator( + request, page_number, query=search_query + ) + + context = {} + context['page'] = page + context['page_number'] = page_number + context['paginator'] = paginator + context['search_query'] = search_query + + context = RequestContext(request, context) + return render_to_response(self.template, context_instance=context) + + +class RSSView(View): + template = 'blog/rss.jinja' + + def get(self, request): + limit = 20 + items = blog_utils.get_posts(limit=limit) + context = {} + context['items'] = items + + context = RequestContext(request, context) + return render_to_response( + 'blog/rss.jinja', + context_instance=context, + mimetype='text/xml' + ) diff --git a/shelfzilla/settings/base.py b/shelfzilla/settings/base.py index 35a1c07..2a9e7c0 100644 --- a/shelfzilla/settings/base.py +++ b/shelfzilla/settings/base.py @@ -58,6 +58,7 @@ INSTALLED_APPS = ( # Staticfiles "compressor", + 'ckeditor', # Apps 'shelfzilla.apps._admin', @@ -66,6 +67,7 @@ INSTALLED_APPS = ( 'shelfzilla.apps.homepage', 'shelfzilla.apps.landing', 'shelfzilla.apps.manga', + 'shelfzilla.apps.blog', 'shelfzilla.apps.pjax', ) @@ -261,6 +263,11 @@ SUIT_CONFIG = { 'label': 'Settings', 'icon': 'icon-cog', }, + { + 'app': 'blog', + 'label': 'Blog', + 'icon': 'icon-book', + }, { 'app': 'manga', 'label': 'Manga', @@ -273,3 +280,15 @@ SUIT_CONFIG = { }, ), } + +# +# CKEDITOR +# +CKEDITOR_UPLOAD_PATH = 'ckeditor/' + +CKEDITOR_CONFIGS = { + 'default': { + 'toolbar': 'Standard', + 'width': '100%', + }, +} diff --git a/shelfzilla/themes/bootflat/templates/blog/_layout.html b/shelfzilla/themes/bootflat/templates/blog/_layout.html new file mode 100644 index 0000000..a99d631 --- /dev/null +++ b/shelfzilla/themes/bootflat/templates/blog/_layout.html @@ -0,0 +1,3 @@ +{% extends "_layout.html" %} + +{% block page_title %}{{ block.super }} | Blog{% endblock %} diff --git a/shelfzilla/themes/bootflat/templates/blog/list.html b/shelfzilla/themes/bootflat/templates/blog/list.html new file mode 100644 index 0000000..a70e792 --- /dev/null +++ b/shelfzilla/themes/bootflat/templates/blog/list.html @@ -0,0 +1,24 @@ +{% extends "blog/_layout.html" %} +{% load i18n %} + +{% block main_content %} +
+
    + {% for item in page.object_list %} +
  • + + + +
    +

    {{ item.title }}

    +
    + Por {{ item.author.first_name }} el + +
    +

    {{ item.content|safe }}

    +
    +
  • + {% endfor %} +
+
+{% endblock %} diff --git a/shelfzilla/urls.py b/shelfzilla/urls.py index 6efc1a6..35e6566 100644 --- a/shelfzilla/urls.py +++ b/shelfzilla/urls.py @@ -9,10 +9,12 @@ admin.autodiscover() urlpatterns = patterns( '', + url(r'^ckeditor/', include('ckeditor.urls')), url(r'^messages/$', MessagesView.as_view(), name="contrib.messages"), url(r'^$', include('shelfzilla.apps.homepage.urls')), url(r'^', include('shelfzilla.apps.landing.urls')), url(r'^', include('shelfzilla.apps.users.urls')), + url(r'^blog/', include('shelfzilla.apps.blog.urls', namespace='blog')), url(r'^series/', include('shelfzilla.apps.manga.urls.series')), url(r'^volumes/', include('shelfzilla.apps.manga.urls.volumes')), url(r'^publishers/', include('shelfzilla.apps.manga.urls.publishers')),