diff options
Diffstat (limited to 'app/jrnl')
-rw-r--r-- | app/jrnl/__init__.py | 0 | ||||
-rw-r--r-- | app/jrnl/admin.py | 92 | ||||
-rw-r--r-- | app/jrnl/fields.py | 7 | ||||
-rw-r--r-- | app/jrnl/models.py | 243 | ||||
-rw-r--r-- | app/jrnl/urls.py | 44 | ||||
-rw-r--r-- | app/jrnl/views.py | 89 | ||||
-rw-r--r-- | app/jrnl/widgets.py | 32 |
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)) |