summaryrefslogtreecommitdiff
path: root/app/jrnl
diff options
context:
space:
mode:
Diffstat (limited to 'app/jrnl')
-rw-r--r--app/jrnl/__init__.py0
-rw-r--r--app/jrnl/admin.py92
-rw-r--r--app/jrnl/fields.py7
-rw-r--r--app/jrnl/models.py243
-rw-r--r--app/jrnl/urls.py44
-rw-r--r--app/jrnl/views.py89
-rw-r--r--app/jrnl/widgets.py32
7 files changed, 507 insertions, 0 deletions
diff --git a/app/jrnl/__init__.py b/app/jrnl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/jrnl/__init__.py
diff --git a/app/jrnl/admin.py b/app/jrnl/admin.py
new file mode 100644
index 0000000..15a7512
--- /dev/null
+++ b/app/jrnl/admin.py
@@ -0,0 +1,92 @@
+from django.contrib import admin
+from django import forms
+from django.contrib.gis.admin import OSMGeoAdmin
+
+from .widgets import AdminImageWidget
+from .models import Entry, EntryAside, PostImage, HomepageCurrator
+
+
+class EntryAsideInline(admin.TabularInline):
+ model = EntryAside
+ extra = 1
+
+
+class EntryAsideAdmin(admin.ModelAdmin):
+ pass
+
+
+class BlogEntryForm(forms.ModelForm):
+ class Meta:
+ model = Entry
+ fields = '__all__'
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 50, 'cols': 100}),
+ }
+
+
+class EntryAdmin(OSMGeoAdmin):
+ form = BlogEntryForm
+ inlines = [EntryAsideInline]
+
+ def formfield_for_dbfield(self, db_field, **kwargs):
+ if db_field.name == 'thumbnail' or db_field.name == 'image':
+ field = forms.FileField(widget=AdminImageWidget)
+ else:
+ field = super(EntryAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+ return field
+
+ list_display = ('title', 'pub_date', 'template_name', 'status', 'region', 'location', 'photo_gallery')
+ search_fields = ['title', 'body_markdown']
+ prepopulated_fields = {"slug": ('title',)}
+ list_filter = ('pub_date', 'enable_comments', 'status', 'location__state__country__lux_region')
+ fieldsets = (
+ ('Entry', {
+ 'fields': (
+ 'title',
+ 'body_markdown',
+ ('pub_date', 'status'),
+ 'slug',
+ 'point'
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }
+ ),
+ ('Formatting data', {
+ 'fields': (
+ 'dek',
+ 'meta_description',
+ ('image', 'thumbnail'),
+ 'template_name',
+ 'enable_comments',
+ ),
+ }),
+ )
+ # options for OSM map Using custom ESRI topo map
+ default_lon = -9285175
+ default_lat = 4025046
+ default_zoom = 6
+ units = True
+ scrollable = False
+ map_width = 700
+ map_height = 425
+ map_template = 'gis/admin/osm.html'
+ openlayers_url = '/static/admin/js/OpenLayers.js'
+
+
+class PostImageAdmin(admin.ModelAdmin):
+ list_display = ('title', 'post_image')
+
+
+class HomepageCurratorAdmin(admin.ModelAdmin):
+ filter_horizontal = ('entry_list',)
+ pass
+
+
+admin.site.register(PostImage, PostImageAdmin)
+admin.site.register(EntryAside, EntryAsideAdmin)
+admin.site.register(Entry, EntryAdmin)
+admin.site.register(HomepageCurrator, HomepageCurratorAdmin)
diff --git a/app/jrnl/fields.py b/app/jrnl/fields.py
new file mode 100644
index 0000000..8df2f5e
--- /dev/null
+++ b/app/jrnl/fields.py
@@ -0,0 +1,7 @@
+from django import forms
+
+from .widgets import AdminImageWidget
+
+
+class FileUploadForm(forms.ModelForm):
+ upload = forms.FileField(widget=AdminImageWidget)
diff --git a/app/jrnl/models.py b/app/jrnl/models.py
new file mode 100644
index 0000000..fe7f274
--- /dev/null
+++ b/app/jrnl/models.py
@@ -0,0 +1,243 @@
+import datetime
+import os
+
+from django.contrib.gis.db import models
+from django.utils.html import format_html
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from django.contrib.syndication.views import Feed
+from django.contrib.sitemaps import Sitemap
+from django import forms
+
+# http://freewisdom.org/projects/python-markdown/
+import markdown
+from bs4 import BeautifulSoup
+
+from photos.models import PhotoGallery
+from locations.models import Location
+
+
+def get_upload_path(self, filename):
+ return "images/post-images/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+def get_tn_path(self, filename):
+ return "images/post-thumbnail/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+def image_url_replace(s):
+ s = s.replace('[[base_url]]', settings.IMAGES_URL)
+ return s
+
+
+def extract_images(s):
+ soup = BeautifulSoup(s)
+ imgs = []
+ for img in soup.find_all('img'):
+ imgs.append(img['src'])
+ return imgs
+
+
+class PostImage(models.Model):
+ title = models.CharField(max_length=100)
+ image = models.ImageField(upload_to="%s/%s" % (settings.IMAGES_ROOT, datetime.datetime.today().strftime("%Y")))
+
+ def __unicode__(self):
+ return self.title
+
+ def post_image(self):
+ return format_html('<img src="%s%s" alt="%s" class="postpic"/>' % (
+ settings.IMAGES_URL,
+ self.image.url.split('images')[1].split('/', 1)[1],
+ self.title)
+ )
+
+ post_image.allow_tags = True
+
+
+class Entry(models.Model):
+ title = models.CharField(max_length=200)
+ slug = models.SlugField(unique_for_date='pub_date')
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField()
+ dek = models.TextField(null=True, blank=True)
+ pub_date = models.DateTimeField('Date published')
+ enable_comments = models.BooleanField(default=False)
+ point = models.PointField(null=True, blank=True)
+ location = models.ForeignKey(Location, null=True, blank=True)
+ PUB_STATUS = (
+ (0, 'Draft'),
+ (1, 'Published'),
+ )
+ status = models.IntegerField(choices=PUB_STATUS, default=0)
+ photo_gallery = models.ForeignKey(PhotoGallery, blank=True, null=True, verbose_name='photo set')
+ image = models.FileField(upload_to=get_upload_path, null=True, blank=True, help_text="should be 205px high")
+ thumbnail = models.FileField(upload_to=get_tn_path, null=True, blank=True, help_text="should be 160 wide")
+ meta_description = models.CharField(max_length=256, null=True, blank=True)
+ TEMPLATES = (
+ (0, 'single'),
+ (1, 'double'),
+ (2, 'single-dark'),
+ (3, 'double-dark'),
+ (4, 'bigimg'),
+ (5, 'bigimg-dark'),
+ )
+ template_name = models.IntegerField(choices=TEMPLATES, default=0)
+
+ class Meta:
+ ordering = ('-pub_date',)
+ get_latest_by = 'pub_date'
+ verbose_name_plural = 'entries'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ # return "/jrnl/%s/%s" % (self.pub_date.strftime("%Y/%m").lower(), self.slug)
+ return reverse("jrnl:detail", kwargs={"year": self.pub_date.year, "month": self.pub_date.strftime("%m"), "slug": self.slug})
+
+ def get_absolute_url_old(self):
+ return "/%s/%s/" % (self.pub_date.strftime("%Y/%b/%d").lower(), self.slug)
+
+ def comment_period_open(self):
+ return self.enable_comments and datetime.datetime.today() - datetime.timedelta(30) <= self.pub_date
+
+ def get_thumbnail_url(self):
+ image_dir, img = self.thumbnail.url.split('post-thumbnail/')[1].split('/')
+ return '%spost-thumbnail/%s/%s' % (settings.IMAGES_URL, image_dir, img)
+
+ def get_image_url(self):
+ image_dir, img = self.image.url.split('post-images/')[1].split('/')
+ return '%spost-images/%s/%s' % (settings.IMAGES_URL, image_dir, img)
+
+ def get_image_wide_url(self):
+ img = self.image.url.split('post-images/')[1].split('/')[1]
+ # return '%shome-images/%s' % (settings.IMAGES_URL, img)
+ return '/media/images/home-images/%s' % (img)
+
+ def get_image_hero_url(self):
+ img = self.image.url.split('post-images/')[1].split('/')[1]
+ return '/media/images/home-images/hero%s' % (img)
+
+ def get_image_hero_url_sm(self):
+ img = self.image.url.split('post-images/')[1].split('/')[1]
+ img = os.path.splitext(img)[0]
+ return '/media/images/home-images/hero%s_sm.jpg' % (img)
+
+ def get_images(self):
+ return extract_images(self.body_html)
+
+ @property
+ def state(self):
+ return self.location.state
+
+ @property
+ def country(self):
+ return self.location.state.country
+
+ @property
+ def region(self):
+ return self.location.state.country.lux_region
+
+ @property
+ def longitude(self):
+ '''Get the site's longitude.'''
+ return self.point.x
+
+ @property
+ def latitude(self):
+ '''Get the site's latitude.'''
+ return self.point.y
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date(status__exact=1)
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date(status__exact=1)
+
+ def save(self):
+ md = image_url_replace(self.body_markdown)
+ self.body_html = markdown.markdown(md, extensions=['extra'], safe_mode=False)
+ self.dek == markdown.markdown(self.dek, safe_mode=False)
+ try:
+ self.location = Location.objects.filter(geometry__contains=self.point).get()
+ except Location.DoesNotExist:
+ raise forms.ValidationError("There is no location associated with that point, add it: %sadmin/locations/location/add/" % (settings.BASE_URL))
+ super(Entry, self).save()
+
+
+class EntryAside(models.Model):
+ title = models.CharField(max_length=200)
+ body = models.TextField(null=True, blank=True)
+ entry = models.ForeignKey(Entry)
+
+
+class HomepageCurrator(models.Model):
+ alt_text = models.CharField(max_length=200)
+ image_base_url = models.CharField(max_length=200)
+ tag_line = models.CharField(max_length=200)
+ banner = models.ForeignKey(Entry, related_name="banner")
+ entry_list = models.ManyToManyField(Entry)
+ template_name = models.CharField(max_length=200, help_text="full path")
+
+
+class BlogSitemap(Sitemap):
+ changefreq = "never"
+ priority = 1.0
+ protocol = "https"
+
+ def items(self):
+ return Entry.objects.filter(status=1)
+
+ def lastmod(self, obj):
+ return obj.pub_date
+
+
+class LatestFull(Feed):
+ title = "Luxagraf: Topographical Writings"
+ link = "/writing/"
+ description = "Latest postings to luxagraf.net"
+ description_template = 'feeds/blog_description.html'
+
+ def items(self):
+ return Entry.objects.filter(status__exact=1).order_by('-pub_date')[:10]
+
+
+import urllib.request
+import urllib.parse
+import urllib.error
+from django_gravatar.helpers import get_gravatar_url, has_gravatar, calculate_gravatar_hash
+from django.dispatch import receiver
+from django_comments.signals import comment_was_posted
+from django_comments.models import Comment
+from django_comments.moderation import CommentModerator, moderator
+
+
+class EntryModerator(CommentModerator):
+ '''
+ Moderate everything except people with multiple approvals
+ '''
+ email_notification = True
+
+ def moderate(self, comment, content_object, request):
+ previous_approvals = Comment.objects.filter(user_email=comment.email, is_public=True).count()
+ if previous_approvals > 2:
+ return False
+ # do entry build right here so it goes to live site
+ return True
+
+moderator.register(Entry, EntryModerator)
+
+
+@receiver(comment_was_posted, sender=Comment)
+def cache_gravatar(sender, comment, **kwargs):
+ gravatar_exists = has_gravatar(comment.email)
+ grav_dir = settings.IMAGES_ROOT + '/gravcache/'
+ if gravatar_exists:
+ url = get_gravatar_url(comment.email, size=60)
+ if not os.path.isdir(grav_dir):
+ os.makedirs(grav_dir)
+ local_grav = '%s/%s.jpg' % (grav_dir, calculate_gravatar_hash(comment.email))
+ urllib.request.urlretrieve(url, local_grav)
diff --git a/app/jrnl/urls.py b/app/jrnl/urls.py
new file mode 100644
index 0000000..d8fa395
--- /dev/null
+++ b/app/jrnl/urls.py
@@ -0,0 +1,44 @@
+from django.conf.urls import url
+from django.views.generic.base import RedirectView
+
+from . import views
+
+urlpatterns = [
+ url(
+ regex=r'(?P<year>\d{4})/(?P<month>\d{2})/(?P<slug>[-\w]+)$',
+ view=views.EntryDetailView.as_view(),
+ name="detail"
+ ),
+ url(
+ regex=r'^(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$',
+ view=views.EntryMonthArchiveView.as_view(month_format='%m'),
+ name="list_month"
+ ),
+ url(
+ regex=r'(?P<year>\d{4})/$',
+ view=views.EntryYearArchiveView.as_view(),
+ name="list_year"
+ ),
+ url(
+ regex=r'(?P<slug>[-\w]+)/(?P<page>\d+)/$',
+ view=views.EntryCountryList.as_view(),
+ name="list_country"
+ ),
+ url(
+ regex=r'(?P<page>\d+)/$',
+ view=views.EntryList.as_view(),
+ name="list"
+ ),
+ # redirect /slug/ to /slug/1/ for live server
+ url(
+ regex=r'(?P<slug>[-\w]+)/$',
+ view=RedirectView.as_view(url="/jrnl/%(slug)s/1/"),
+ name="live_location_redirect"
+ ),
+ # redirect / to /1/ for live server
+ url(
+ regex=r'',
+ view=RedirectView.as_view(url="/jrnl/1/"),
+ name="live_redirect"
+ ),
+]
diff --git a/app/jrnl/views.py b/app/jrnl/views.py
new file mode 100644
index 0000000..7dd755b
--- /dev/null
+++ b/app/jrnl/views.py
@@ -0,0 +1,89 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.views.generic.dates import YearArchiveView, MonthArchiveView
+
+from django.conf import settings
+
+from .models import Entry, HomepageCurrator
+
+from locations.models import Country
+
+
+class EntryList(ListView):
+ """
+ Return a list of Entries in reverse chronological order
+ """
+ context_object_name = 'object_list'
+ queryset = Entry.objects.filter(status__exact=1).order_by('-pub_date').select_related()
+ template_name = "archives/writing.html"
+
+ def dispatch(self, request, *args, **kwargs):
+ request.page_url = '/jrnl/%d/'
+ request.page = int(self.kwargs['page'])
+ return super(EntryList, self).dispatch(request, *args, **kwargs)
+
+
+class EntryCountryList(ListView):
+ """
+ Return a list of Entries by Country in reverse chronological order
+ """
+ context_object_name = 'object_list'
+ template_name = "archives/writing.html"
+
+ def dispatch(self, request, *args, **kwargs):
+ request.page_url = '/jrnl/' + self.kwargs['slug'] + '/%d/'
+ request.page = int(self.kwargs['page'])
+ return super(EntryCountryList, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(EntryCountryList, self).get_context_data(**kwargs)
+ context['region'] = Country.objects.get(slug__exact=self.kwargs['slug'])
+ return context
+
+ def get_queryset(self):
+ country = Country.objects.get(slug__exact=self.kwargs['slug'])
+ return Entry.objects.filter(
+ status__exact=1,
+ location__state__country=country
+ ).order_by('-pub_date')
+
+
+class EntryYearArchiveView(YearArchiveView):
+ queryset = Entry.objects.filter(status__exact=1).select_related()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+ template_name = "archives/writing_date.html"
+
+
+class EntryMonthArchiveView(MonthArchiveView):
+ queryset = Entry.objects.filter(status__exact=1).select_related()
+ date_field = "pub_date"
+ allow_future = True
+ template_name = "archives/writing_date.html"
+
+
+class EntryDetailView(DetailView):
+ model = Entry
+ template_name = "details/entry.html"
+ slug_field = "slug"
+
+
+class HomepageList(ListView):
+ """
+ Return a main entry and list of Entries in reverse chronological order
+ """
+ context_object_name = 'recent'
+ queryset = Entry.objects.filter(status__exact=1)[:4]
+
+ def get_template_names(self):
+ obj = HomepageCurrator.objects.get(pk=1)
+ return ['%s' % obj.template_name]
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(HomepageList, self).get_context_data(**kwargs)
+ context['homepage'] = HomepageCurrator.objects.get(pk=1)
+ context['IMAGES_URL'] = settings.IMAGES_URL
+ return context
diff --git a/app/jrnl/widgets.py b/app/jrnl/widgets.py
new file mode 100644
index 0000000..030437b
--- /dev/null
+++ b/app/jrnl/widgets.py
@@ -0,0 +1,32 @@
+import os
+
+from django.contrib.admin.widgets import AdminFileWidget
+from django.utils.safestring import mark_safe
+from django.conf import settings
+
+
+def thumbnail(image_path):
+ absolute_url = os.path.join(settings.IMAGES_URL, image_path[7:])
+ print(absolute_url)
+ return '<img style="max-width: 400px" src="%s" alt="%s" />' % (absolute_url, image_path)
+
+
+class AdminImageWidget(AdminFileWidget):
+ """
+ A FileField Widget that displays an image instead of a file path
+ if the current file is an image.
+ """
+ def render(self, name, value, attrs=None):
+ output = []
+ file_name = str(value)
+ help_text = ''
+ if file_name:
+ file_path = '%s' % (file_name)
+ if attrs['id'] == 'id_thumbnail':
+ help_text = '160 wide'
+ if attrs['id'] == 'id_image':
+ help_text = '205px high'
+ output.append('<span>%s</span><a target="_blank" href="%s">%s</a>' % (help_text, file_path, thumbnail(file_name)))
+
+ output.append(super(AdminFileWidget, self).render(name, value, attrs))
+ return mark_safe(''.join(output))