From bb3973ffb714c932e9ec6dd6a751228dc71fe1d3 Mon Sep 17 00:00:00 2001 From: lxf Date: Sat, 14 May 2022 16:38:07 -0400 Subject: initial commit --- app/posts/__init__.py | 0 app/posts/admin.py | 77 ++++++ app/posts/build.py | 110 +++++++++ app/posts/importer.py | 107 ++++++++ app/posts/migrations/0001_initial.py | 56 +++++ app/posts/migrations/0002_alter_post_post_type.py | 18 ++ app/posts/migrations/__init__.py | 0 app/posts/models.py | 275 +++++++++++++++++++++ app/posts/templates/horizontal_select.html | 17 ++ app/posts/templates/posts/essay_detail.html | 178 +++++++++++++ app/posts/templates/posts/essay_list.html | 24 ++ .../posts/fieldnote_archive_list_date.html | 43 ++++ app/posts/templates/posts/fieldnote_detail.html | 114 +++++++++ app/posts/templates/posts/fieldnote_list.html | 54 ++++ app/posts/templates/posts/guide_base.html | 41 +++ app/posts/templates/posts/guide_detail.html | 187 ++++++++++++++ app/posts/templates/posts/jrnl_date.html | 42 ++++ app/posts/templates/posts/jrnl_detail.html | 240 ++++++++++++++++++ app/posts/templates/posts/jrnl_detail.txt | 8 + app/posts/templates/posts/jrnl_latest.html | 6 + app/posts/templates/posts/jrnl_list.html | 33 +++ app/posts/templates/posts/post_detail.html | 183 ++++++++++++++ app/posts/templates/posts/post_detail.txt | 8 + app/posts/templates/posts/post_list.html | 40 +++ app/posts/templates/posts/src_detail.html | 110 +++++++++ app/posts/templates/posts/src_list.html | 30 +++ app/posts/urls/__init__old.py | 4 + app/posts/urls/essay_urls.py | 29 +++ app/posts/urls/field_note_urls.py | 39 +++ app/posts/urls/guide_urls.py | 30 +++ app/posts/urls/guide_urls_old.py | 29 +++ app/posts/urls/jrnl_urls.py | 60 +++++ app/posts/urls/review_urls.py | 29 +++ app/posts/urls/src_urls.py | 49 ++++ app/posts/views/__init__.py | 19 ++ app/posts/views/field_note_views.py | 39 +++ app/posts/views/guide_views.py | 96 +++++++ app/posts/views/jrnl_views.py | 173 +++++++++++++ app/posts/views/src_views.py | 97 ++++++++ 39 files changed, 2694 insertions(+) create mode 100644 app/posts/__init__.py create mode 100644 app/posts/admin.py create mode 100644 app/posts/build.py create mode 100644 app/posts/importer.py create mode 100644 app/posts/migrations/0001_initial.py create mode 100644 app/posts/migrations/0002_alter_post_post_type.py create mode 100644 app/posts/migrations/__init__.py create mode 100644 app/posts/models.py create mode 100644 app/posts/templates/horizontal_select.html create mode 100644 app/posts/templates/posts/essay_detail.html create mode 100644 app/posts/templates/posts/essay_list.html create mode 100644 app/posts/templates/posts/fieldnote_archive_list_date.html create mode 100644 app/posts/templates/posts/fieldnote_detail.html create mode 100644 app/posts/templates/posts/fieldnote_list.html create mode 100644 app/posts/templates/posts/guide_base.html create mode 100644 app/posts/templates/posts/guide_detail.html create mode 100644 app/posts/templates/posts/jrnl_date.html create mode 100644 app/posts/templates/posts/jrnl_detail.html create mode 100644 app/posts/templates/posts/jrnl_detail.txt create mode 100644 app/posts/templates/posts/jrnl_latest.html create mode 100644 app/posts/templates/posts/jrnl_list.html create mode 100644 app/posts/templates/posts/post_detail.html create mode 100644 app/posts/templates/posts/post_detail.txt create mode 100644 app/posts/templates/posts/post_list.html create mode 100644 app/posts/templates/posts/src_detail.html create mode 100644 app/posts/templates/posts/src_list.html create mode 100644 app/posts/urls/__init__old.py create mode 100644 app/posts/urls/essay_urls.py create mode 100644 app/posts/urls/field_note_urls.py create mode 100644 app/posts/urls/guide_urls.py create mode 100644 app/posts/urls/guide_urls_old.py create mode 100644 app/posts/urls/jrnl_urls.py create mode 100644 app/posts/urls/review_urls.py create mode 100644 app/posts/urls/src_urls.py create mode 100644 app/posts/views/__init__.py create mode 100644 app/posts/views/field_note_views.py create mode 100644 app/posts/views/guide_views.py create mode 100644 app/posts/views/jrnl_views.py create mode 100644 app/posts/views/src_views.py (limited to 'app/posts') diff --git a/app/posts/__init__.py b/app/posts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/posts/admin.py b/app/posts/admin.py new file mode 100644 index 0000000..af39162 --- /dev/null +++ b/app/posts/admin.py @@ -0,0 +1,77 @@ +from django.contrib import admin +from django import forms +from django.contrib.gis.admin import OSMGeoAdmin +from django.contrib.contenttypes.admin import GenericStackedInline + +from utils.widgets import AdminImageWidget, LGEntryForm +from .models import Post + +from utils.util import get_latlon + + +@admin.register(Post) +class PostAdmin(OSMGeoAdmin): + form = LGEntryForm + + def render_change_form(self, request, context, *args, **kwargs): + #context['adminform'].form.fields['featured_image'].queryset = LuxImage.objects.all()[:200] + return super(PostAdmin, self).render_change_form(request, context, *args, **kwargs) + + def formfield_for_dbfield(self, db_field, **kwargs): + if db_field.name == 'thumbnail' or db_field.name == 'image': + field = forms.FileField(widget=AdminImageWidget) + elif db_field.name == 'meta_description': + field = forms.CharField(widget=forms.Textarea(attrs={'rows': 4, 'cols': 75})) + field.required = False + else: + field = super(PostAdmin, self).formfield_for_dbfield(db_field, **kwargs) + return field + + list_display = ('title', 'site', 'post_type', 'pub_date', 'template_name', 'status',) + search_fields = ['title', 'body_markdown'] + prepopulated_fields = {"slug": ('title',)} + list_filter = ('site', 'post_type', 'pub_date', 'enable_comments', 'status') + fieldsets = ( + ('Entry', { + 'fields': ( + ('title', 'short_title'), + 'subtitle', + 'body_markdown', + ('pub_date', 'status', 'post_type'), + ('slug', 'enable_comments',), + 'dek', + 'meta_description', + 'template_name', + ('featured_image','related'), + 'site' + ), + 'classes': ( + 'show', + 'extrapretty', + 'wide' + ) + } + ), + ('Extras', { + 'fields': ( + ('has_video', 'disclaimer',), + 'topics', + 'prologue_markdown', + 'epilogue_markdown', + 'originally_published_by', + 'originally_published_by_url', + ), + 'classes': ( + 'collapse', + ) + }), + ) + + class Media: + js = ('image-loader.js', 'product-loader.js', 'next-prev-links.js') + css = { + "all": ("my_styles.css",) + } + + + diff --git a/app/posts/build.py b/app/posts/build.py new file mode 100644 index 0000000..47a6efe --- /dev/null +++ b/app/posts/build.py @@ -0,0 +1,110 @@ +from django.urls import reverse +from django.apps import apps +from builder.base import BuildNew +from itertools import chain + +from django.conf import settings +from .models import PostType + + +class BuildSrc(BuildNew): + + def get_model_queryset(self): + return self.model.objects.filter(post_type=PostType.SRC).filter(status__exact=1).order_by('-pub_date') + + def build(self): + self.build_list_view( + base_path=reverse("src:list"), + paginate_by=50 + ) + self.build_detail_view() + + +class BuildGuide(BuildNew): + + def get_model_queryset(self): + return self.model.objects.filter(post_type__in=[PostType.FIELD_TEST, PostType.REVIEW]).filter(status__exact=1).order_by('-pub_date') + + def build(self): + self.build_list_view( + base_path=reverse("guides:guide-base"), + paginate_by=50 + ) + self.build_detail_view() + + +class BuildFieldNotes(BuildNew): + + def get_model_queryset(self): + return self.model.objects.filter(post_type=PostType.FIELD_NOTE).filter(status__exact=1).order_by('-pub_date') + + def build(self): + self.build_detail_view() + self.build_list_view( + base_path=reverse("fieldnotes:list"), + paginate_by=24 + ) + self.build_year_view("fieldnotes:list_year") + self.build_month_view("fieldnotes:list_month") + + +class BuildJrnl(BuildNew): + ''' + Write jrnl to disk + ''' + def get_model_queryset(self): + return self.model.objects.filter(post_type=PostType.JRNL).filter(status__exact=1).order_by('-pub_date') + + def build(self): + self.build_list_view( + base_path=reverse("jrnl:list"), + paginate_by=24 + ) + self.build_year_view("jrnl:list_year") + self.build_month_view("jrnl:list_month") + self.build_detail_view() + self.build_location_view() + self.build_latest() + + def build_arc(self): + self.build_list_view( + base_path=reverse("jrnl:list"), + paginate_by=24 + ) + self.build_year_view("jrnl:list_year") + self.build_month_view("jrnl:list_month") + self.build_location_view() + + def build_location_view(self): + c = apps.get_model('locations', 'Country') + r = apps.get_model('locations', 'Region') + countries = c.objects.filter(visited=True) + regions = r.objects.all() + locations = list(chain(countries, regions)) + for c in locations: + try: + qs = self.model.objects.filter( + status__exact=1, + post_type=PostType.JRNL, + location__state__country=c + ) + except: + qs = self.model.objects.filter( + status__exact=1, + post_type=PostType.JRNL, + location__state__country__lux_region=c.id + ) + print(c) + pages = self.get_pages(qs, 24) + for page in range(pages): + base_path = reverse("jrnl:list_country", kwargs={'slug': c.slug, 'page': page + 1}) + response = self.client.get(base_path) + print(response.content) + if page == 0: + self.write_file(base_path, response.content) + else: + self.write_file(base_path, response.content) + + def build_latest(self): + response = self.client.get('/jrnl/latest/') + self.write_file(reverse("jrnl:latest"), response.content) diff --git a/app/posts/importer.py b/app/posts/importer.py new file mode 100644 index 0000000..7ed4782 --- /dev/null +++ b/app/posts/importer.py @@ -0,0 +1,107 @@ +for e in essaysold: + if e.featured_image: + feat = e.featured_image + else: + feat = None + if e.meta_description: + meta = e.meta_description + else: + meta = "need meta" + new, created = Post.objects.get_or_create( + old_id=e.pk, + post_type=2, + title=e.title, + subtitle=e.sub_title, + dek=e.dek, + slug=e.slug, + prologue_markdown=e.preamble, + body_markdown=e.body_markdown, + pub_date=e.pub_date, + enable_comments=e.enable_comments, + status=e.status, + meta_description=meta, + originally_published_by=e.originally_published_by, + originally_published_by_url=e.originally_published_by_url, + featured_image=feat, + has_video=e.has_video, + epilogue_markdown=e.afterword, + ) + print(created) + + + +# migrate jrnl to posts +for e in Entry.objects.all(): + if e.meta_description: + meta_description = e.meta_description + else: + meta_description = "needs" + if e.image: + old_image = e.image + else: + old_image = None + p, created = Post.objects.get_or_create( + old_id=e.pk, + title = e.title, + short_title = '', + subtitle = e.subtitle, + slug = e.slug, + body_markdown = e.body_markdown, + body_html = e.body_html, + dek = e.dek, + meta_description = meta_description, + pub_date = e.pub_date, + enable_comments = e.enable_comments, + status = e.status, + featured_image = e.featured_image, + post_type = PostType.JRNL, + template_name = e.template_name, + has_video = e.has_video, + point = e.point, + location = e.location, + old_image=old_image + ) + for b in e.books.all(): + c = Book.objects.get( + slug=b.slug, + title=b.title, + ) + p.books.add(c) + for f in e.field_notes.all(): + c = Post.objects.get( + slug=f.slug, + title=f.title, + ) + p.field_notes.add(c) + p.save() + +#Then after they're all in there: +ctype = ContentType.objects.get(app_label='posts',model='post') +oldctype = ContentType.objects.get(app_label='jrnl',model='entry') +for e in Entry.objects.all(): + p = Post.objects.get(title=e.title,old_id=e.id) + if e.related: + for t in e.related.all(): + if t.model_name == oldctype: + tp = ctype + else: + tp = t.model_name + c = RelatedPost.objects.get( + model_name=tp, + title=t.title, + slug=t.slug, + pub_date=t.pub_date + ) + p.related.add(c) + p.save() + +# Then to port comments: +ctype = ContentType.objects.get(app_label='posts',model='post') +oldctype = ContentType.objects.get(app_label='jrnl',model='entry') +for c in Comment.objects.filter(content_type=oldctype): + e = Entry.objects.get(pk=c.object_pk) + p = Post.objects.get(title=e.title,old_id=e.id) + c.object_pk = p.pk + c.content_type = ctype + print("%s --> %s" %(c.content_object,p)) + c.save() diff --git a/app/posts/migrations/0001_initial.py b/app/posts/migrations/0001_initial.py new file mode 100644 index 0000000..fe0faa8 --- /dev/null +++ b/app/posts/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.8 on 2021-10-06 20:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('taxonomy', '0002_auto_20211006_2025'), + ('normalize', '0002_alter_relatedpost_id'), + ('sites', '0002_alter_domain_unique'), + ('media', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('short_title', models.CharField(blank=True, max_length=200, null=True)), + ('subtitle', models.CharField(blank=True, max_length=200)), + ('slug', models.SlugField(unique_for_date='pub_date')), + ('prologue_markdown', models.TextField(blank=True, null=True)), + ('prologue_html', models.TextField(blank=True, null=True)), + ('body_markdown', models.TextField()), + ('body_html', models.TextField(blank=True)), + ('epilogue_markdown', models.TextField(blank=True, null=True)), + ('epilogue_html', models.TextField(blank=True, null=True)), + ('dek', models.TextField(blank=True, null=True)), + ('meta_description', models.CharField(blank=True, max_length=256)), + ('pub_date', models.DateTimeField(verbose_name='Date published')), + ('last_updated', models.DateTimeField(auto_now=True)), + ('enable_comments', models.BooleanField(default=False)), + ('status', models.IntegerField(choices=[(0, 'Draft'), (1, 'Published')], default=0)), + ('post_type', models.IntegerField(choices=[(0, 'field test'), (1, 'review'), (2, 'essay'), (3, 'src'), (4, 'jrnl'), (5, 'field note')], default=4)), + ('template_name', models.IntegerField(choices=[(0, 'single'), (1, 'double'), (2, 'single-dark'), (3, 'double-dark'), (4, 'single-black'), (5, 'double-black')], default=0)), + ('has_video', models.BooleanField(blank=True, default=False)), + ('has_code', models.BooleanField(blank=True, default=False)), + ('disclaimer', models.BooleanField(blank=True, default=False)), + ('originally_published_by', models.CharField(blank=True, max_length=400, null=True)), + ('originally_published_by_url', models.CharField(blank=True, max_length=400, null=True)), + ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='media.luximage')), + ('related', models.ManyToManyField(blank=True, to='normalize.RelatedPost')), + ('site', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sites.site')), + ('topics', models.ManyToManyField(blank=True, to='taxonomy.Category')), + ], + options={ + 'ordering': ('-pub_date',), + 'get_latest_by': 'pub_date', + }, + ), + ] diff --git a/app/posts/migrations/0002_alter_post_post_type.py b/app/posts/migrations/0002_alter_post_post_type.py new file mode 100644 index 0000000..0502bf0 --- /dev/null +++ b/app/posts/migrations/0002_alter_post_post_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.8 on 2022-02-04 20:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('posts', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='post', + name='post_type', + field=models.IntegerField(choices=[(0, 'Homepage'), (1, 'review'), (2, 'essay'), (3, 'src'), (4, 'jrnl'), (5, 'field note')], default=4), + ), + ] diff --git a/app/posts/migrations/__init__.py b/app/posts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/posts/models.py b/app/posts/models.py new file mode 100644 index 0000000..040fe52 --- /dev/null +++ b/app/posts/models.py @@ -0,0 +1,275 @@ +import datetime +import os + +from django.dispatch import receiver +from django.contrib.gis.db import models +from django.db.models.signals import post_save +from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.contrib.sites.models import Site +from django.urls import reverse +from django.utils.functional import cached_property +from django.apps import apps +from django.conf import settings +from django.contrib.sitemaps import Sitemap +from django import forms + +import urllib.request +import urllib.parse +import urllib.error +from django_gravatar.helpers import get_gravatar_url, has_gravatar, calculate_gravatar_hash +from django_comments.signals import comment_was_posted +from django_comments.models import Comment +from django_comments.moderation import CommentModerator, moderator + +from taggit.managers import TaggableManager + +from normalize.models import RelatedPost +from media.models import LuxImage, LuxImageSize +#from fieldnotes.models import FieldNote +from taxonomy.models import TaggedItems, Category +from utils.util import render_images, render_products, parse_video, markdown_to_html, extract_main_image + + +def get_upload_path(self, filename): + return "images/post-images/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename) + + +class PostType(models.IntegerChoices): + HOMEPAGE = 0, ('Homepage') + REVIEW = 1, ('review') + ESSAY = 2, ('essay') + SRC = 3, ('src') + JRNL = 4, ('jrnl') + FIELD_NOTE = 5, ('field note') + + +class Post(models.Model): + site = models.ForeignKey(Site, on_delete=models.CASCADE) + title = models.CharField(max_length=200) + short_title = models.CharField(max_length=200, blank=True, null=True) + subtitle = models.CharField(max_length=200, blank=True) + slug = models.SlugField(unique_for_date='pub_date') + prologue_markdown = models.TextField(blank=True, null=True) + prologue_html = models.TextField(blank=True, null=True) + body_markdown = models.TextField() + body_html = models.TextField(blank=True) + epilogue_markdown = models.TextField(blank=True, null=True) + epilogue_html = models.TextField(blank=True, null=True) + dek = models.TextField(null=True, blank=True) + meta_description = models.CharField(max_length=256, blank=True) + pub_date = models.DateTimeField('Date published') + last_updated = models.DateTimeField(auto_now=True) + enable_comments = models.BooleanField(default=False) + PUB_STATUS = ( + (0, 'Draft'), + (1, 'Published'), + ) + status = models.IntegerField(choices=PUB_STATUS, default=0) + featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True) + TEMPLATES = ( + (0, 'single'), + (1, 'double'), + (2, 'single-dark'), + (3, 'double-dark'), + (4, 'single-black'), + (5, 'double-black'), + ) + post_type = models.IntegerField(choices=PostType.choices, default=PostType.JRNL) + template_name = models.IntegerField(choices=TEMPLATES, default=0) + has_video = models.BooleanField(blank=True, default=False) + has_code = models.BooleanField(blank=True, default=False) + disclaimer = models.BooleanField(blank=True, default=False) + related = models.ManyToManyField(RelatedPost, blank=True) + topics = models.ManyToManyField(Category, blank=True) + originally_published_by = models.CharField(max_length=400, null=True, blank=True) + originally_published_by_url = models.CharField(max_length=400, null=True, blank=True) + + class Meta: + ordering = ('-pub_date',) + get_latest_by = 'pub_date' + + def __str__(self): + return self.title + + def get_absolute_url(self): + if self.post_type == 0: + return reverse('guides:reviews:review-detail', kwargs={"slug": self.slug}) + if self.post_type == 1: + return reverse('guides:reviews:review-detail', kwargs={"slug": self.slug}) + if self.post_type == 2: + return reverse('essays:detail', kwargs={"slug": self.slug}) + if self.post_type == 3: + return reverse('src:detail', kwargs={"slug": self.slug}) + if self.post_type == 5: + return reverse('fieldnote:detail', kwargs={"year": self.pub_date.year, "month": self.pub_date.strftime("%m"), "slug": self.slug}) + if self.post_type == PostType.JRNL: + return reverse('jrnl:detail', kwargs={"year": self.pub_date.year, "month": self.pub_date.strftime("%m"), "slug": self.slug}) + + def comment_period_open(self): + return self.enable_comments and datetime.datetime.today() - datetime.timedelta(30) <= self.pub_date + + def get_featured_image_thumb(self): + return self.featured_image.get_image_by_size("tn") + + def get_content_type(self): + return ContentType.objects.get(app_label="posts", model="post") + + @property + def get_previous_published(self): + return self.get_previous_by_pub_date(status__exact=1,post_type=self.post_type) + + @property + def get_previous_admin_url(self): + n = self.get_previous_by_pub_date() + return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[n.id] ) + + @property + def get_next_published(self): + return self.get_next_by_pub_date(status__exact=1,post_type=self.post_type) + + @property + def get_next_admin_url(self): + model = apps.get_model(app_label=self._meta.app_label, model_name=self._meta.model_name) + try: + return reverse('admin:%s_%s_change' %(self._meta.app_label, self._meta.model_name), args=[self.get_next_by_pub_date().pk] ) + except model.DoesNotExist: + return '' + + @property + def longitude(self): + '''Get the site's longitude.''' + if self.point: + return self.point.x + + @property + def latitude(self): + '''Get the site's latitude.''' + if self.point: + return self.point.y + + @property + def sitemap_priority(self): + if self.post_type in [2,4,5]: + return 1.0 + else: + return 0.7 + + def get_image_url(self): + ''' + for legacy jrnl posts without a featured_image + ''' + try: + image_dir, img = self.old_image.url.split('post-images/')[1].split('/') + return '%spost-images/%s/%s' % (settings.IMAGES_URL, image_dir, img) + except ValueError: + pass + + def save(self, *args, **kwargs): + created = self.pk is None + if not created: + md = render_images(self.body_markdown) + prods = render_products(md) + print(self.title) + self.body_html = markdown_to_html(prods) + if self.epilogue_html: + self.epilogue_html = markdown_to_html(self.epilogue_markdown) + if self.prologue_html: + self.prologue_html = markdown_to_html(self.prologue_markdown) + self.has_video = parse_video(self.body_html) + if created and not self.featured_image: + if self.post_type == PostType.FIELD_NOTE: + self.featured_image = extract_main_image(self.body_markdown) + else: + self.featured_image = LuxImage.objects.latest() + old = type(self).objects.get(pk=self.pk) if self.pk else None + if old and old.featured_image != self.featured_image: # Field has changed + if self.featured_image: + s = LuxImageSize.objects.get(name="featured_jrnl") + ss = LuxImageSize.objects.get(name="picwide-med") + self.featured_image.sizes.add(s) + self.featured_image.sizes.add(ss) + self.featured_image.save() + if old and old.title != self.title or old and old.slug != self.slug: + related, c = RelatedPost.objects.get_or_create(model_name=self.get_content_type(), entry_id = self.id, pub_date=self.pub_date) + related.title = self.title + related.slug = self.slug + related.save() + super(Post, self).save(*args, **kwargs) + + +class PostModerator(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) + for approval in previous_approvals: + if approval.submit_date <= datetime.datetime.today() - datetime.timedelta(21): + approve = True + if previous_approvals.count() > 2 and approve: + return False + # do entry build right here so it goes to live site + return True +moderator.register(Post, PostModerator) + + +@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) + + +@receiver(post_save, sender=Post) +def post_save_events(sender, update_fields, created, instance, **kwargs): + related, created = RelatedPost.objects.get_or_create(model_name=instance.get_content_type(), entry_id = instance.id, pub_date=instance.pub_date, title=instance.title, slug=instance.slug) + post_save.disconnect(post_save_events, sender=Post) + instance.save() + post_save.connect(post_save_events, sender=Post) + + +class PostSitemap(Sitemap): + changefreq = "never" + protocol = "https" + + def items(self): + return Post.objects.filter(status=1) + + def lastmod(self, obj): + return obj.pub_date + + def priority(self, obj): + return obj.sitemap_priority + + +""" +for p in src: + s, created = Post.objects.get_or_create( + old_id=p.id, + title=p.title, + slug=p.slug, + body_markdown=p.body_markdown, + pub_date=p.pub_date, + enable_comments=p.enable_comments, + has_code=p.has_code, + status=p.status, + meta_description=p.meta_description, + post_type=3, + ) + print(p) + for t in p.topics.all(): + c,created = Category.objects.get_or_create( + slug=t.slug, + name=t.name, + pluralized_name=t.pluralized_name + ) + s.topics.add(c) +""" diff --git a/app/posts/templates/horizontal_select.html b/app/posts/templates/horizontal_select.html new file mode 100644 index 0000000..61dcfd8 --- /dev/null +++ b/app/posts/templates/horizontal_select.html @@ -0,0 +1,17 @@ +{% with id=widget.attrs.id %} + + {% for group, options, index in widget.optgroups %} + {% if group %} +
  • {{ group }} + + {% endif %} + {% for option in options %} +
  • {% include option.template_name with widget=option %}
  • + {% endfor %} + {% if group %} + + + {% endif %} + {% endfor %} + +{% endwith %} diff --git a/app/posts/templates/posts/essay_detail.html b/app/posts/templates/posts/essay_detail.html new file mode 100644 index 0000000..e95c161 --- /dev/null +++ b/app/posts/templates/posts/essay_detail.html @@ -0,0 +1,178 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} +{%block htmlclass%}class="detail single"{%endblock%} +{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %} + +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} +{% if object.has_code %} + +{%endif %} + + + + + + + + + + + + + + + {% if object.featured_image %} + {%endif%} + +{%endblock%} + +{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%} +{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %} +
    +
    +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}

    +

    {{object.subtitle|smartypants|safe}}

    +
    + {% if object.originally_published_by %}

    Originally Published By: {{object.originally_published_by}}

    {%endif%} + {% if object.location %}
    +

    {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name|safe}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name|safe}}{%endif%}

    + – Map +
    {%endif%} + + +
    +
    +
    + {% if object.prologue_html %}
    + {{object.prologue_html|smartypants|safe}} +
    {%endif%} + {{object.body_html|safe|smartypants}} +
    + {% if object.afterword_html %}
    +

    Afterward

    + {{object.afterword_html|smartypants|safe}} +
    {%endif%} + {%if wildlife or object.field_notes.all or object.books.all %}{%endif%} +
    + + {% comment %}
    +
    If you enjoyed this, you should join the mailing list…
    + {% include 'mailing_list.html' %} +
    {% endcomment %} +
    + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +

    {{comment_count}} Comment{{ comment_count|pluralize }}

    +{% render_comment_list for object %} +{%endif%} +
    +{% render_comment_form for object %} +
    +{% else %} +

    Sorry, comments have been disabled for this post.

    +{%endif%} +{% endblock %} +{% block js %} + +{%endblock%} diff --git a/app/posts/templates/posts/essay_list.html b/app/posts/templates/posts/essay_list.html new file mode 100644 index 0000000..8a35225 --- /dev/null +++ b/app/posts/templates/posts/essay_list.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} + +{% block pagetitle %}Collected Essays of Scott Gilbertson {% endblock %} +{% block metadescription %}Collected writing: essays, articles and stories on travel, photography, tools, walking, the natural world and other ephemera.{% endblock %} +{% block breadcrumbs %}{% if breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{%endif%}{% endblock %} +{% block primary %}
    +
    +

    Essays & Articles

    +

    Topics include travel, writing, photography, free software, culture, and once, Del Taco.

    +

    Some essays below were previously published in: WIRED, Budget Travel, Ars Technica, Epicurious, Longshot Magazine, The Cost of Paper and elsewhere.

    +
    +

    Essays

    + +
    +{%endblock%} diff --git a/app/posts/templates/posts/fieldnote_archive_list_date.html b/app/posts/templates/posts/fieldnote_archive_list_date.html new file mode 100644 index 0000000..5d6865f --- /dev/null +++ b/app/posts/templates/posts/fieldnote_archive_list_date.html @@ -0,0 +1,43 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load html5_datetime %} +{% block pagetitle %} Field Notes | luxagraf {% endblock %} +{% block metadescription %} Rough notes and sketches from the field {% endblock %} +{%block bodyid%}id="field-notes"{%endblock%} + +{% block primary %} + +
    +
    +

    Field Notes {% if month or year %}{% if month %} from {{month|date:"F"}} {{month|date:"Y"}}{%else%} from {{year|date:"Y"}}{%endif%}{%endif%}

    +

    Quick notes, sketches and images from the road. This is the semi-orgnized brain dump that comes before the more organized journal entries and essays. If I used social media this is the stuff I'd probably put there, but I prefer to put it here, even if it means a lot few people read it.

    +
    + +
    + +{% endblock %} + + + diff --git a/app/posts/templates/posts/fieldnote_detail.html b/app/posts/templates/posts/fieldnote_detail.html new file mode 100644 index 0000000..3368c56 --- /dev/null +++ b/app/posts/templates/posts/fieldnote_detail.html @@ -0,0 +1,114 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load html5_datetime %} +{% load month_number_to_name %} +{% block pagetitle %}{{object.title|title|smartypants|safe}} - Luxagraf, Field Notes{% endblock %} + +{% block metadescription %}{{object.body_html|striptags|safe|truncatewords:30}}{% endblock %} +{%block extrahead%} + + + + + +{%endblock%} +{% block bodyid %}class="notes--permalin detail" id="archive-{% if month %}{{month|month_number_to_name}}{%endif%}{{year}}"{%endblock%} +{% block breadcrumbs %} + +{% endblock %} +{% block primary %}
    +
    +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}

    + {% if object.subtitle %}

    {{object.subtitle|smartypants|safe}}

    {%endif%} +
    + {% if object.location %}
    +

    {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name|safe}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name|safe}}{%endif%}

    + – Map +
    {%endif%} + + +
    +
    +
    + {{object.body_html|safe|smartypants}} +
    + + + + + + + + {% with object.get_next_published as next %} + {% with object.get_previous_published as prev %} + {%endwith%}{%endwith%} +
    +
    +{% endblock %} + +{% block js %} + +{%endblock%} diff --git a/app/posts/templates/posts/fieldnote_list.html b/app/posts/templates/posts/fieldnote_list.html new file mode 100644 index 0000000..14d61c0 --- /dev/null +++ b/app/posts/templates/posts/fieldnote_list.html @@ -0,0 +1,54 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load get_next %} +{% load html5_datetime %} +{% load pagination_tags %} +{% block pagetitle %} Field Notes | luxagraf {% endblock %} +{% block metadescription %}Rough notes and sketches from the field {% endblock %} +{%block bodyid%}id="field-notes"{%endblock%} +{% block breadcrumbs %} + +{% endblock %} +{% block primary %}
    +
    +

    Field Notes

    +

    Quick notes, sketches, and images from the road. This is the semi-organized brain dump that comes before the more organized journal entries. If I used social media this is the stuff I'd probably put there, but I prefer to put it here, even if it means a lot fewer people read it.

    +
    + {% autopaginate object_list 24 %} + +
    + +{% endblock %} + + + diff --git a/app/posts/templates/posts/guide_base.html b/app/posts/templates/posts/guide_base.html new file mode 100644 index 0000000..e7764db --- /dev/null +++ b/app/posts/templates/posts/guide_base.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load html5_datetime %} +{% load pagination_tags %} +{% block pagetitle %}Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.{% endblock %} +{% block metadescription %}Guides for fellow travelers: tools, tips, and tricks to make life on the road in an RV or Van easier and more enjoyable.{% endblock %} + +{% block primary %} +
    +
    +

    Roaming Guide

    +

    Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.

    +

    I don't want to tell you how to travel. Everyone is different. Besides, even after twenty some odd years of travel, I am still learning.

    +

    I've always been most inspired by wandering monks and nuns, those who walked or sailed with next to nothing and survived. Mostly. Today most of us are not that skilled or strong of will, but keeping that example in mind is helpful. The less stuff you travel with the better off you are. Up to a point. Having the right tools is important. The right tools make life easier and more fun.

    +

    I put this together to help you find the tools you need. These aren't casual reviews. These are things I have spent years seeking out, using, and refining. In the end what you need are not things, but strategies and tools that allow you to create solutions to problems.

    +
    +

    Reviews

    + {% autopaginate object_list 30 %} + +
    +{%endblock%} diff --git a/app/posts/templates/posts/guide_detail.html b/app/posts/templates/posts/guide_detail.html new file mode 100644 index 0000000..7e5df74 --- /dev/null +++ b/app/posts/templates/posts/guide_detail.html @@ -0,0 +1,187 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} +{%block htmlclass%}class="detail single"{%endblock%} +{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %} + +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} +{% if object.has_code %} + +{%endif %} + + + + + + + + + + + + + + + {% if object.featured_image %} + {%endif%} + +{%endblock%} + +{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%} + +{% block primary %} +
    + +
    + + +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}

    +

    {{object.subtitle|smartypants|safe}}

    +
    + {% if object.originally_published_by %}

    Originally Published By: {{object.originally_published_by}}

    {%endif%} + {% if object.location %}
    +

    {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name|safe}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name|safe}}{%endif%}

    + – Map +
    {%endif%} +

    Filed Under: Guides, {% for topic in object.topics.all %}{{topic}}{%endfor%}

    + +
    +
    +
    + {% if object.preamble %}
    + {{object.preamble_html|smartypants|safe}} +
    {%endif%} + {{object.body_html|safe|smartypants}} +
    + {% if object.afterword_html %}
    +

    Afterward

    + {{object.afterword_html|smartypants|safe}} +
    {%endif%} + {%if wildlife or object.field_notes.all or object.books.all %}{%endif%} +
    + + {% comment %}
    +
    If you enjoyed this, you should join the mailing list…
    + {% include 'mailing_list.html' %} +
    {% endcomment %} +
    + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +

    {{comment_count}} Comment{{ comment_count|pluralize }}

    +{% render_comment_list for object %} +{%endif%} +
    +{% render_comment_form for object %} +
    +{% else %} +

    Sorry, comments have been disabled for this post.

    +{%endif%} +{% endblock %} +{% block js %} + +{%endblock%} diff --git a/app/posts/templates/posts/jrnl_date.html b/app/posts/templates/posts/jrnl_date.html new file mode 100644 index 0000000..dba1c53 --- /dev/null +++ b/app/posts/templates/posts/jrnl_date.html @@ -0,0 +1,42 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load html5_datetime %} +{% block pagetitle %} Field Notes | luxagraf {% endblock %} +{% block metadescription %} Rough notes and sketches from the field {% endblock %} +{%block bodyid%}id="field-notes"{%endblock%} + +{% block primary %} + +
    +
    +

    Journal {% if month or year %}{% if month %} from {{month|date:"F"}} {{month|date:"Y"}}{%else%} from {{year|date:"Y"}}{%endif%}{%endif%}

    +
    + +
    + +{% endblock %} + + + diff --git a/app/posts/templates/posts/jrnl_detail.html b/app/posts/templates/posts/jrnl_detail.html new file mode 100644 index 0000000..8a75f25 --- /dev/null +++ b/app/posts/templates/posts/jrnl_detail.html @@ -0,0 +1,240 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} + +{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %} + +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} + + + + + + + + + + + + + + + + + + + + + + +{%endblock%} +{%block htmlclass%}{% with object.template_name as t %} +class="detail {%if t == 1 or t == 3 or t == 5 %}double{%else%}single{%endif%}{%if t == 2 or t == 3 %} dark{%endif%}{%if t == 4 or t == 5 %} black{%endif%}"{%endwith%}{%endblock%} + + {% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %} +
    +
    +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}

    + {% if object.subtitle %}

    {{object.subtitle|smartypants|safe}}

    {%endif%} +
    + {% if object.location %}
    +

    {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name|safe}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name|safe}}{%endif%}

    + – Map +
    {%endif%} + + +
    +
    +
    + {{object.body_html|safe|smartypants}} +
    + {%if wildlife or object.field_notes.all or object.books.all %}{%endif%} +
    + {% with object.get_next_published as next %} + {% with object.get_previous_published as prev %} + + {% if object.related.all %}
    + +
    {%endif%} + + {% comment %}
    +
    If you enjoyed this, you should join the mailing list…
    + {% include 'mailing_list.html' %} +
    {% endcomment %} + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +

    {{comment_count}} Comment{{ comment_count|pluralize }}

    +{% render_comment_list for object %} +{%endif%} +
    +{% render_comment_form for object %} +
    +{% else %} +

    Sorry, comments have been disabled for this post.

    +{%endif%} +
    +{% endblock %} +{% block js %} + +{%endblock%} diff --git a/app/posts/templates/posts/jrnl_detail.txt b/app/posts/templates/posts/jrnl_detail.txt new file mode 100644 index 0000000..a608b88 --- /dev/null +++ b/app/posts/templates/posts/jrnl_detail.txt @@ -0,0 +1,8 @@ +{{object.title|safe}} +{% for letter in object.title %}={%endfor%} + + by Scott Gilbertson + + {{object.pub_date|date:"l, d F Y"}} + +{{object.body_markdown|safe}} diff --git a/app/posts/templates/posts/jrnl_latest.html b/app/posts/templates/posts/jrnl_latest.html new file mode 100644 index 0000000..03e3c56 --- /dev/null +++ b/app/posts/templates/posts/jrnl_latest.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block js %} + +{% endblock %} diff --git a/app/posts/templates/posts/jrnl_list.html b/app/posts/templates/posts/jrnl_list.html new file mode 100644 index 0000000..6eefe10 --- /dev/null +++ b/app/posts/templates/posts/jrnl_list.html @@ -0,0 +1,33 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load pagination_tags %} +{% block pagetitle %}Luxagraf | {% if region %}Travel Writing from {{region.name|title|smartypants|safe}}{%else%}Travel Writing from Around the World {%endif%}{% if page != "1" %} -- Page {{page}}{%endif%}{% endblock %} +{% block metadescription %}{% if region %}Travel writing, essays and dispatches from {{region.name|title|smartypants|safe}}{%else%}Travel writing, essays and dispatches from around the world{%endif%} Page {{page}}{% endblock %} +{%block bodyid%}id="writing" class="archive"{%endblock%} +{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %}
    +

    {% if region %}Journal entries from {%if region.name == 'United States'%}the United States{%else%}{{region.name|title|smartypants|safe}}{%endif%}{%else%}Journal {%endif%}

    {% autopaginate object_list 24 %} {% for object in object_list %} +
    +
    + {% if object.featured_image %} + {% include "lib/img_archive.html" with image=object.featured_image %} + {%else%} + {{ object.title }}{%endif%} +
    +

    {{object.title|safe|smartypants|widont}}

    + + +

    + + {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name}}{%endif%} + – + + {{object.dek|safe}} + +

    +
    {% endfor %} +
    + +{% endblock %} diff --git a/app/posts/templates/posts/post_detail.html b/app/posts/templates/posts/post_detail.html new file mode 100644 index 0000000..9e82786 --- /dev/null +++ b/app/posts/templates/posts/post_detail.html @@ -0,0 +1,183 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} +{%block htmlclass%}class="detail single"{%endblock%} +{% block pagetitle %}{{object.title|title|smartypants|safe}} - by Scott Gilbertson{% endblock %} + +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} +{% if object.has_code %} + +{%endif %} + + + + + + + + + + + + + + + {% if object.featured_image %} + {%endif%} + +{%endblock%} + +{%block bodyid %}{% if object.get_post_type_display == 'tools' %}class="src"{% endif %}{%endblock%} + +{% block primary %} +
    + +
    +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|smartypants|safe}}{%else%}{{object.title|smartypants|safe}}{%endif%}

    +

    {{object.subtitle|smartypants|safe}}

    +
    + {% if object.originally_published_by %}

    Originally Published By: {{object.originally_published_by}}

    {%endif%} + {% if object.location %}
    +

    {% if object.location.country_name == "United States" %}{{object.location.name|smartypants|safe}}, {{object.location.state_name|safe}}, U.S.{%else%}{{object.location.name|smartypants|safe}}, {{object.location.country_name|safe}}{%endif%}

    + – Map +
    {%endif%} + + +
    +
    +
    + {% if object.preamble %}
    + {{object.preamble_html|smartypants|safe}} +
    {%endif%} + {{object.body_html|safe|smartypants}} +
    + {% if object.afterword_html %}
    +

    Afterward

    + {{object.afterword_html|smartypants|safe}} +
    {%endif%} + {%if wildlife or object.field_notes.all or object.books.all %}{%endif%} +
    + + {% comment %}
    +
    If you enjoyed this, you should join the mailing list…
    + {% include 'mailing_list.html' %} +
    {% endcomment %} +
    + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +

    {{comment_count}} Comment{{ comment_count|pluralize }}

    +{% render_comment_list for object %} +{%endif%} +
    +{% render_comment_form for object %} +
    +{% else %} +

    Sorry, comments have been disabled for this post.

    +{%endif%} +{% endblock %} +{% block js %} + +{%endblock%} diff --git a/app/posts/templates/posts/post_detail.txt b/app/posts/templates/posts/post_detail.txt new file mode 100644 index 0000000..547ce79 --- /dev/null +++ b/app/posts/templates/posts/post_detail.txt @@ -0,0 +1,8 @@ +{{object.title|safe}} +{% for letter in object.title %}={%endfor%} + + by Scott Gilbertson + <{{SITE_URL}}{{object.get_absolute_url}}> + {{object.pub_date|date:"l, d F Y"}} + +{{object.body_markdown|safe}} diff --git a/app/posts/templates/posts/post_list.html b/app/posts/templates/posts/post_list.html new file mode 100644 index 0000000..9b3a1f3 --- /dev/null +++ b/app/posts/templates/posts/post_list.html @@ -0,0 +1,40 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load html5_datetime %} +{% load pagination_tags %} +{% block pagetitle %}Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.{% endblock %} +{% block metadescription %}Guides for fellow travelers: tools, tips, and tricks to make life on the road in an RV or Van easier and more enjoyable.{% endblock %} + +{% block primary %} +
    +
    +

    Field Tests

    +

    Advice, Tools, Tips, and Tricks for Full Time Van or RV Life.

    +

    After {{years_on_the_road}} years on the road, here's what I know: all you really need is a vehicle, a stove and a cooler. After that, it's all luxury. The less stuff you travel with the better off you are. Up to a point. Having the right tools is important. The right tools make life easier and more fun.

    +

    I put this together to help you find the tools you need. These aren't casual reviews. These are things I have spent years seeking out, using, and refining. In the end what you need are not things, but solutions to problems.

    +
    +

    Reviews

    + {% autopaginate object_list 30 %} + +
    +{%endblock%} diff --git a/app/posts/templates/posts/src_detail.html b/app/posts/templates/posts/src_detail.html new file mode 100644 index 0000000..664db36 --- /dev/null +++ b/app/posts/templates/posts/src_detail.html @@ -0,0 +1,110 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} +{% block pagetitle %}{{object.title|striptags}} - by Scott Gilbertson{% endblock %} +{% block metadescription %}{% autoescape on %}{{object.meta_description|striptags|safe}}{% endautoescape %}{% endblock %} +{%block extrahead%} + + + + + + + + + + + + + + +{%endblock%} + +{% block bodyid %}class="src detail single"{% endblock %} +{%block sitesubtitle %}Code Slowly{% endblock%} +{% block breadcrumbs %}{% endblock %} +{% block primary %}
    +
    +
    +

    {%if object.template_name == 1 or object.template_name == 3 %}{{object.title|safe|smartypants}}{%else%}{{object.title|safe|smartypants|widont}}{%endif%}

    +

    {% if object.subtitle %}{{object.subtitle|smartypants|safe}}{%else%}{{object.meta_description|safe|smartypants|widont}}{%endif%}

    +
    + {% if object.originally_published_by %}

    Originally Published By: {{object.originally_published_by}}

    {%endif%} + + +
    +
    + + +
    + {{object.body_html|safe|smartypants|widont}} +
    +
    + {% if object.slug != 'about' %} + {% with object.get_next_published as next %} + {% with object.get_previous_published as prev %} + {%endwith%}{%endwith%} + {%endif%} +
    + {% if object.slug != 'about' %} + {% if object.enable_comments %} +{% get_comment_count for object as comment_count %} +{%if comment_count > 0 %} +

    {{comment_count}} Comment{{ comment_count|pluralize }}

    +{% render_comment_list for object %} +{%endif%} +
    +{% render_comment_form for object %} +
    +{% else %} +

    Sorry, comments have been disabled for this post.

    +{%endif%} +{%endif%} +{% endblock %} +{% block js %} + +{% if object.has_code %} +{%endif %} +{% endblock %} diff --git a/app/posts/templates/posts/src_list.html b/app/posts/templates/posts/src_list.html new file mode 100644 index 0000000..21a8c4e --- /dev/null +++ b/app/posts/templates/posts/src_list.html @@ -0,0 +1,30 @@ +{% extends 'base.html' %} +{% load typogrify_tags %} +{% load comments %} + +{% block pagetitle %}Tutorials and tools for building great things{% endblock %} +{% block metadescription %}Tutorials and tools for building great things on the web - by Scott Gilbertson.{% endblock %} +{%block sitesubtitle %}Code Slowly{% endblock%} +{% block breadcrumbs %}{% include "lib/breadcrumbs.html" with breadcrumbs=breadcrumbs %}{% endblock %} +{% block primary %}
    +
    +

    Tutorials and tools for building great things on the web.

    +

    The indie web is an amazing democratic publishing platform unlike anything in history. The catch is, to avoid serving at the pleasure of the corporate king, you need to know how to publish. That's what these articles are here for, to help you learn how to use independent, community supported open source tools. The web won't last forever, let's build something cool while we can.

    +

    Topics include HTML, CSS, Django, Linux, Nginx, Python, Postgresql, free software, and, once, the evil that is Google AMP.

    +

    A few of the articles below were previously published in: Ars Technica, Wired, and The Register

    +
    +

    Articles

    + + + + +
    +{%endblock%} diff --git a/app/posts/urls/__init__old.py b/app/posts/urls/__init__old.py new file mode 100644 index 0000000..4993c34 --- /dev/null +++ b/app/posts/urls/__init__old.py @@ -0,0 +1,4 @@ +from .guide_urls import urlpatterns as guide_urlpatterns +from .reviews_urls import urlpatterns as review_urlpatterns + +urlpatterns = review_urlpatterns + guide_urlpatterns diff --git a/app/posts/urls/essay_urls.py b/app/posts/urls/essay_urls.py new file mode 100644 index 0000000..3f0d7d7 --- /dev/null +++ b/app/posts/urls/essay_urls.py @@ -0,0 +1,29 @@ +from django.urls import path, re_path + +from ..views import guide_views as views + +app_name = "essays" + +urlpatterns = [ + path( + r'', + views.PostDetailView.as_view(), + name="detail" + ), + path( + r'.txt', + views.PostDetailViewTXT.as_view(), + name="detail-txt" + ), + path( + r'/', + views.EssayListView.as_view(), + name="list" + ), + path( + r'', + views.EssayListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/posts/urls/field_note_urls.py b/app/posts/urls/field_note_urls.py new file mode 100644 index 0000000..7352b0e --- /dev/null +++ b/app/posts/urls/field_note_urls.py @@ -0,0 +1,39 @@ +from django.urls import path, re_path + +from ..views import field_note_views as views + +app_name = "fieldnotes" + +urlpatterns = [ + re_path( + r'(?P[0-9]{4})/$', + views.FieldNoteYearArchiveView.as_view(), + name="list_year" + ), + path( + r'', + views.FieldNoteListView.as_view(), + {'page': 1}, + name="list" + ), + path( + r'/', + views.FieldNoteListView.as_view(), + name="list" + ), + path( + r'//.txt', + views.FieldNoteDetailViewTXT.as_view(), + name="detail-txt" + ), + path( + r'//', + views.FieldNoteDetailView.as_view(), + name="detail" + ), + path( + r'//', + views.FieldNoteMonthArchiveView.as_view(month_format='%m'), + name="list_month" + ), +] diff --git a/app/posts/urls/guide_urls.py b/app/posts/urls/guide_urls.py new file mode 100644 index 0000000..e0a2210 --- /dev/null +++ b/app/posts/urls/guide_urls.py @@ -0,0 +1,30 @@ +from django.urls import path, re_path, include +from django.views.generic.base import RedirectView + +from ..views import guide_views as views + +app_name = "guides" + +urlpatterns = [ + path( + r'', + views.GuideListView.as_view(), + {'page':1}, + name="guide-base" + ), + path(r'field-test/', include('posts.urls.review_urls', namespace='reviews')), + re_path(r'^field-test/$', RedirectView.as_view(url='/field-tests/')), + #path(r'field-tests/', include('posts.urls', namespace='review-list')), + #path(r'review/', include('posts.review_urls')), + re_path(r'^review/$', RedirectView.as_view(url='/guides/')), + #path( + # r'', + # views.EntryDetailView.as_view(), + # name="detail" + #), + #re_path( + # r'/', + # views.EntryList.as_view(), + # name="list" + #), +] diff --git a/app/posts/urls/guide_urls_old.py b/app/posts/urls/guide_urls_old.py new file mode 100644 index 0000000..d43d76a --- /dev/null +++ b/app/posts/urls/guide_urls_old.py @@ -0,0 +1,29 @@ +from django.urls import path, re_path + +from . import views + +app_name = "guides" + +urlpatterns = [ + path( + r'', + views.PostDetailView.as_view(), + name="detail" + ), + path( + r'.txt', + views.PostDetailViewTXT.as_view(), + name="detail-txt" + ), + path( + r'/', + views.GuidesListView.as_view(), + name="list" + ), + path( + r'', + views.GuidesListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/posts/urls/jrnl_urls.py b/app/posts/urls/jrnl_urls.py new file mode 100644 index 0000000..d7f0fb1 --- /dev/null +++ b/app/posts/urls/jrnl_urls.py @@ -0,0 +1,60 @@ +from django.urls import path, re_path + +from ..views import jrnl_views as views + +app_name = "jrnl" + +urlpatterns = [ + path( + r'feed.xml', + views.JrnlRSSFeedView(), + name="feed" + ), + re_path( + r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+).txt$', + views.JrnlDetailViewTXT.as_view(), + name="detail-txt" + ), + re_path( + r'^(?P\d{4})/(?P\d{2})/(?P[-\w]+)$', + views.JrnlDetailView.as_view(), + name="detail" + ), + re_path( + r'^(?P[0-9]{4})/(?P[0-9]{2})/$', + views.JrnlMonthArchiveView.as_view(month_format='%m'), + name="list_month" + ), + re_path( + r'(?P\d{4})/$', + views.JrnlYearArchiveView.as_view(), + name="list_year" + ), + re_path( + r'^(?P\d+)/$', + views.JrnlListView.as_view(), + name="list" + ), + path( + r'latest/', + views.JrnlLatestView.as_view(), + name="latest" + ), + re_path( + r'(?P[-\w]+)/(?P\d+)/$', + views.JrnlCountryListView.as_view(), + name="list_country" + ), + re_path( + r'^(?P[-\w]+)/$', + views.JrnlCountryListView.as_view(), + {'page':1}, + name="list_country" + ), + re_path( + r'', + views.JrnlListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/posts/urls/review_urls.py b/app/posts/urls/review_urls.py new file mode 100644 index 0000000..6b78e43 --- /dev/null +++ b/app/posts/urls/review_urls.py @@ -0,0 +1,29 @@ +from django.urls import path, re_path + +from ..views import guide_views as views + +app_name = "reviews" + +urlpatterns = [ + path( + r'', + views.PostDetailView.as_view(), + name="review-detail" + ), + path( + r'.txt', + views.PostDetailViewTXT.as_view(), + name="review-detail-txt" + ), + path( + r'/', + views.ReviewsListView.as_view(), + name="review-list" + ), + path( + r'', + views.ReviewsListView.as_view(), + {'page':1}, + name="review-list" + ), +] diff --git a/app/posts/urls/src_urls.py b/app/posts/urls/src_urls.py new file mode 100644 index 0000000..637d9a6 --- /dev/null +++ b/app/posts/urls/src_urls.py @@ -0,0 +1,49 @@ +from django.urls import path, re_path + +from ..views import src_views as views + +app_name = "src" + +urlpatterns = [ + path( + r'feed.xml', + views.SrcRSSFeedView(), + name="feed" + ), + path( + r'topic/', + views.TopicListView.as_view(), + name="list_topics" + ), + #path( + # r'books/', + # views.BookDetailView.as_view(), + # name='detail_book' + #), + #path( + # r'books/', + # views.BookListView.as_view(), + # name='list_books' + #), + path( + r'.txt', + views.SrcDetailViewTXT.as_view(), + name="detail-txt" + ), + path( + r'', + views.SrcDetailView.as_view(), + name="detail" + ), + re_path( + r'', + views.SrcListView.as_view(), + name="list" + ), + path( + r'', + views.SrcListView.as_view(), + {'page':1}, + name="list" + ), +] diff --git a/app/posts/views/__init__.py b/app/posts/views/__init__.py new file mode 100644 index 0000000..5fd984c --- /dev/null +++ b/app/posts/views/__init__.py @@ -0,0 +1,19 @@ +from django.contrib.syndication.views import Feed + +from ..models import Post + +class PostRSSFeedView(Feed): + title = "Luxagraf: Topographical Writings" + link = "https://luxagraf.net/" + description = "Latest postings to luxagraf.net" + description_template = 'feeds/blog_description.html' + + def items(self): + return Post.objects.filter(status__exact=1).order_by('-pub_date')[:10] + + def item_pubdate(self, item): + """ + Takes an item, as returned by items(), and returns the item's + pubdate. + """ + return item.pub_date diff --git a/app/posts/views/field_note_views.py b/app/posts/views/field_note_views.py new file mode 100644 index 0000000..41ceeaa --- /dev/null +++ b/app/posts/views/field_note_views.py @@ -0,0 +1,39 @@ +from django.views.generic.dates import YearArchiveView, MonthArchiveView +from django.views.generic.detail import DetailView + +from utils.views import PaginatedListView, LuxDetailView + +from ..models import Post, PostType + + +class FieldNoteListView(PaginatedListView): + model = Post + template_name = "posts/fieldnote_list.html" + """ + Return a list of Notes in reverse chronological order + """ + queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1).order_by('-pub_date') + + +class FieldNoteDetailView(LuxDetailView): + model = Post + slug_field = "slug" + template_name = "posts/fieldnote_detail.html" + + +class FieldNoteDetailViewTXT(FieldNoteDetailView): + template_name = "posts/entry_detail.txt" + + +class FieldNoteYearArchiveView(YearArchiveView): + queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1) + date_field = "pub_date" + template_name = "posts/fieldnote_archive_list_date.html" + make_object_list = True + + +class FieldNoteMonthArchiveView(MonthArchiveView): + queryset = Post.objects.filter(post_type=PostType.FIELD_NOTE,status=1) + date_field = "pub_date" + make_object_list = True + template_name = "posts/fieldnote_archive_list_date.html" diff --git a/app/posts/views/guide_views.py b/app/posts/views/guide_views.py new file mode 100644 index 0000000..e2f98f3 --- /dev/null +++ b/app/posts/views/guide_views.py @@ -0,0 +1,96 @@ +from django.views.generic import ListView +from django.views.generic.detail import DetailView +from django.contrib.syndication.views import Feed +from django.apps import apps +from django.conf import settings + +from utils.views import PaginatedListView, LuxDetailView + +from ..models import Post, PostType +from taxonomy.models import Category + + +class GuideListView(PaginatedListView): + """ + Return a list of Entries in reverse chronological order + """ + model = Post + template_name = "posts/guide_base.html" + + def get_queryset(self): + queryset = super(GuideListView, self).get_queryset() + return queryset.filter(status__exact=1).filter(post_type__in=[PostType.REVIEW,PostType.FIELD_TEST]).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image') + + +class ReviewsListView(GuideListView): + template_name = "posts/post.html" + + def get_queryset(self): + queryset = super(ReviewsListView, self).get_queryset() + return queryset.filter(post_type__in=[0,1]).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image') + + def get_context_data(self, **kwargs): + context = super(ReviewsListView, self).get_context_data(**kwargs) + context['archive_type'] = 'Field Tests' + return context + + +class EssayListView(PaginatedListView): + model = Post + template_name = "posts/essay_list.html" + + def get_queryset(self): + queryset = super(EssayListView, self).get_queryset() + return queryset.filter(site__domain='luxagraf.net').filter(post_type__in=[PostType.ESSAY]).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image') + + def get_context_data(self, **kwargs): + ''' + override for custom breadcrumb path + ''' + # Call the base implementation first to get a context + context = super(EssayListView, self).get_context_data(**kwargs) + context['breadcrumbs'] = ('Essays',) + return context + + +class PostDetailView(LuxDetailView): + model = Post + slug_field = "slug" + + def get_queryset(self): + queryset = super(PostDetailView, self).get_queryset() + return queryset.select_related('location').prefetch_related('field_notes') + + def get_context_data(self, **kwargs): + context = super(PostDetailView, self).get_context_data(**kwargs) + related = [] + for obj in self.object.related.all(): + model = apps.get_model(obj.post_model.app_label, obj.post_model.model) + related.append(model.objects.get(slug=obj.slug)) + context['related'] = related + return context + + def get_template_names(self): + obj = self.get_object() + return ["posts/%s_detail.html" % obj.get_post_type_display(), 'posts/post_detail.html'] + + +class PostDetailViewTXT(PostDetailView): + template_name = "posts/entry_detail.txt" + + +class PostsRSSFeedView(Feed): + title = "VanLifeReviews.com: " + link = "/" + description = "Latest reviews, stories and guides" + description_template = 'feeds/blog_description.html' + + def items(self): + return Post.objects.filter(status__exact=1).order_by('-pub_date')[:10] + + def item_pubdate(self, item): + """ + Takes an item, as returned by items(), and returns the item's + pubdate. + """ + return item.pub_date diff --git a/app/posts/views/jrnl_views.py b/app/posts/views/jrnl_views.py new file mode 100644 index 0000000..0dc2dc8 --- /dev/null +++ b/app/posts/views/jrnl_views.py @@ -0,0 +1,173 @@ +from django.views.generic import ListView +from django.views.generic.detail import DetailView +from django.views.generic.dates import DateDetailView +from django.urls import reverse +from django.views.generic.dates import YearArchiveView, MonthArchiveView +from django.contrib.syndication.views import Feed +from django.apps import apps +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.db.models import Q + +from utils.views import PaginatedListView + +#from ..models import Entry, HomepageCurrator, Home +from ..models import Post, PostType +from locations.models import LuxCheckIn, Country, Region, Location +from sightings.models import Sighting + + +class JrnlListView(PaginatedListView): + """ + Return a list of Entries in reverse chronological order + """ + model = Post + template_name = "posts/jrnl_list.html" + queryset = Post.objects.filter(post_type=PostType.JRNL).filter(status__exact=1).order_by('-pub_date').prefetch_related('location').prefetch_related('featured_image') + + def get_context_data(self, **kwargs): + context = super(JrnlListView, self).get_context_data(**kwargs) + context['breadcrumbs'] = ['jrnl',] + return context + +class JrnlCountryListView(PaginatedListView): + """ + Return a list of Entries by Country in reverse chronological order + """ + model = Post + template_name = "posts/jrnl_list.html" + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(JrnlCountryListView, self).get_context_data(**kwargs) + try: + context['region'] = Country.objects.get(slug__exact=self.kwargs['slug']) + except: + context['region'] = Region.objects.get(slug__exact=self.kwargs['slug']) + context['breadcrumbs'] = ['jrnl',] + return context + + def get_queryset(self): + try: + region = Country.objects.get(slug__exact=self.kwargs['slug']) + qs = Post.objects.filter( + post_type=PostType.JRNL, + status__exact=1, + location__state__country=region + ).order_by('-pub_date') + except: + region = Region.objects.get(slug__exact=self.kwargs['slug']) + qs = Post.objects.filter( + post_type=PostType.JRNL, + status__exact=1, + location__state__country__lux_region=region.id + ).order_by('-pub_date') + return qs + + +class JrnlYearArchiveView(YearArchiveView): + queryset = Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).select_related() + date_field = "pub_date" + make_object_list = True + allow_future = True + template_name = "posts/jrnl_date.html" + + +class JrnlMonthArchiveView(MonthArchiveView): + queryset = Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).select_related() + date_field = "pub_date" + allow_future = True + template_name = "posts/jrnl_date.html" + + +class JrnlDetailView(DateDetailView): + model = Post + date_field = 'pub_date' + slug_field = "slug" + template_name = "posts/jrnl_detail.html" + + def get_queryset(self): + queryset = super(JrnlDetailView, self).get_queryset() + return queryset.filter(post_type=PostType.JRNL).select_related('location').prefetch_related('field_notes').prefetch_related('books') + + def get_object(self, queryset=None): + obj = get_object_or_404( + self.model, + slug=self.kwargs['slug'], + pub_date__month=self.kwargs['month'], + pub_date__year=self.kwargs['year'] + ) + self.location = obj.location + return obj + + def get_context_data(self, **kwargs): + context = super(JrnlDetailView, self).get_context_data(**kwargs) + context['wildlife'] = Sighting.objects.filter( + Q(location=self.location) | + Q(location__in=Location.objects.filter(parent=self.location)) + ).select_related().order_by('ap_id', 'ap__apclass__kind').distinct("ap") + related = [] + for obj in self.object.related.all(): + model = apps.get_model(obj.model_name.app_label, obj.model_name.model) + related.append(model.objects.get(slug=obj.slug, pub_date=obj.pub_date)) + context['related'] = related + context['breadcrumbs'] = ("jrnl",) + context['crumb_url'] = reverse('jrnl:list') + return context + + +class JrnlDetailViewTXT(JrnlDetailView): + template_name = "posts/jrnl_detail.txt" + + +class JrnlLatestView(JrnlDetailView): + template_name = "posts/jrnl_latest.html" + + def get_object(self, queryset=None): + obj = self.model.objects.filter(status=1).latest() + self.location = obj.location + return obj + + +class JrnlRSSFeedView(Feed): + title = "Luxagraf: Topographical Writings" + link = "/jrnl/" + description = "Latest postings to luxagraf.net" + description_template = 'feeds/blog_description.html' + + def items(self): + return Post.objects.filter(status__exact=1).filter(post_type=PostType.JRNL).order_by('-pub_date')[:10] + + def item_pubdate(self, item): + """ + Takes an item, as returned by items(), and returns the item's + pubdate. + """ + return item.pub_date + +''' +class HomepageList(ListView): + """ + Return a main entry and list of Entries in reverse chronological order + """ + model = Entry + + def get_home(self): + return Home.objects.filter(pk=1).prefetch_related('featured_image').select_related('featured').select_related('featured__location').get() + + def get_queryset(self): + queryset = super(HomepageList, self).get_queryset() + self.home = self.get_home() + return queryset.filter(status__exact=1).order_by('-pub_date').exclude().select_related('location').select_related('featured_image')[1:9] + + def get_template_names(self): + return ['%s' % self.home.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'] = self.home + context['location'] = LuxCheckIn.objects.latest() + context['IMAGES_URL'] = settings.IMAGES_URL + return context +''' diff --git a/app/posts/views/src_views.py b/app/posts/views/src_views.py new file mode 100644 index 0000000..990088f --- /dev/null +++ b/app/posts/views/src_views.py @@ -0,0 +1,97 @@ +from django.views.generic import ListView +from django.views.generic.detail import DetailView +from django.contrib.syndication.views import Feed +from django.urls import reverse +from django.conf import settings + +#from paypal.standard.forms import PayPalPaymentsForm +from utils.views import PaginatedListView +from ..models import Post, PostType +from taxonomy.models import Category + + +class SrcListView(PaginatedListView): + model = Post + template_name="posts/src_list.html" + + def get_queryset(self): + queryset = super(SrcListView, self).get_queryset() + return queryset.filter(post_type=PostType.SRC).filter(status__exact=1).order_by('-pub_date') + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(SrcListView, self).get_context_data(**kwargs) + #context['topics'] = Topic.objects.all() + context['breadcrumbs'] = ['src',] + return context + + +class SrcDetailView(DetailView): + model = Post + slug_field = "slug" + template_name="posts/src_detail.html" + + +class SrcDetailViewTXT(SrcDetailView): + template_name = "posts/jrnl_detail.txt" + + +class TopicListView(ListView): + template_name = 'posts/topic_list.html' + + def get_queryset(self): + return Post.objects.filter(topics__slug=self.kwargs['slug'],post_type=PostType.SRC) + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(TopicListView, self).get_context_data(**kwargs) + context['topic'] = Category.objects.get(slug__exact=self.kwargs['slug']) + return context + + +class SrcRSSFeedView(Feed): + title = "luxagraf:src Code and Technology" + link = "/src/" + description = "Latest postings to luxagraf.net/src" + description_template = 'feeds/blog_description.html' + + def items(self): + return SrcPost.objects.filter(status__exact=1).order_by('-pub_date')[:10] + + +""" +class BookListView(ListView): + template_name = "archives/src_books.html" + + def queryset(self): + return Book.objects.filter(status__exact=1) + + +class BookDetailView(DetailView): + model = Book + + def get_template_names(self): + book = self.get_object() + return [book.template_name] + + def get_context_data(self, **kwargs): + # Call the base implementation first to get a context + context = super(BookDetailView, self).get_context_data(**kwargs) + book = self.get_object() + if book.price_sale < book.price: + price = book.price_sale + else: + price = book.price + #paypal_dict = { + # "business": settings.PAYPAL_RECEIVER_EMAIL, + # "amount": price, + # "item_name": book.title, + # "invoice": "unique-invoice-id", + # "notify_url": "https://luxagraf.net/src/paypal/" + reverse('src:paypal-ipn'), + # "return_url": "https://luxagraf.net/src/thank-you", + # "cancel_return": "https://luxagraf.net/src/books/", + #} + #context['paypal_form'] = PayPalPaymentsForm(initial=paypal_dict) + return context + +""" -- cgit v1.2.3-70-g09d2