summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore16
-rw-r--r--app/blog/__init__.py0
-rw-r--r--app/blog/admin.py46
-rw-r--r--app/blog/build.py22
-rw-r--r--app/blog/migrations/0001_initial.py43
-rw-r--r--app/blog/migrations/__init__.py0
-rw-r--r--app/blog/models.py70
-rw-r--r--app/blog/urls.py28
-rw-r--r--app/blog/views.py47
-rw-r--r--app/builder/__init__.py0
-rw-r--r--app/builder/base.py152
-rw-r--r--app/builder/sanitizer.py60
-rw-r--r--app/builder/views.py13
-rw-r--r--app/pages/__init__.py0
-rw-r--r--app/pages/admin.py32
-rw-r--r--app/pages/build.py39
-rw-r--r--app/pages/migrations/0001_initial.py26
-rw-r--r--app/pages/migrations/0002_page_build.py18
-rw-r--r--app/pages/migrations/__init__.py0
-rw-r--r--app/pages/models.py42
-rw-r--r--app/pages/tests/__init__.py0
-rw-r--r--app/pages/tests/test_models.py36
-rw-r--r--app/pages/tests/test_views.py27
-rw-r--r--app/pages/views.py18
-rw-r--r--app/photos/__init__.py0
-rw-r--r--app/photos/admin.py32
-rw-r--r--app/photos/detail_urls.py10
-rw-r--r--app/photos/forms.py173
-rw-r--r--app/photos/migrations/0001_initial.py73
-rw-r--r--app/photos/migrations/__init__.py0
-rw-r--r--app/photos/models.py223
-rw-r--r--app/photos/photos.js71
-rw-r--r--app/photos/retriever.py.bak314
-rw-r--r--app/photos/static/image-preview.js42
-rw-r--r--app/photos/static/my_styles.css40
-rw-r--r--app/photos/templatetags/__init__.py0
-rw-r--r--app/photos/templatetags/get_image_by_size.py8
-rw-r--r--app/photos/templatetags/get_image_width.py9
-rw-r--r--app/photos/urls.py56
-rw-r--r--app/photos/utils.py28
-rw-r--r--app/photos/views.py59
-rw-r--r--app/taxonomy/migrations/0001_initial.py52
-rw-r--r--app/taxonomy/migrations/__init__.py0
-rw-r--r--app/taxonomy/models.py39
-rw-r--r--app/utils/__init__.py0
-rw-r--r--app/utils/forms.py7
-rw-r--r--app/utils/next_prev.py80
-rw-r--r--app/utils/static/autocomplete.js10
-rw-r--r--app/utils/static/choices.css2
-rw-r--r--app/utils/static/choices.min.js5
-rw-r--r--app/utils/static/image-loader.js47
-rw-r--r--app/utils/static/next-prev-links.js60
-rw-r--r--app/utils/urls.py12
-rw-r--r--app/utils/util.py118
-rw-r--r--app/utils/views.py102
-rw-r--r--app/utils/widgets.py153
-rw-r--r--config/__init__.py0
-rw-r--r--config/base_urls.py35
-rw-r--r--config/django.ini30
-rw-r--r--config/requirements.txt41
-rw-r--r--config/wsgi.py26
-rw-r--r--design/config.rb12
-rw-r--r--design/sass/_content.scss236
-rw-r--r--design/sass/_fonts.scss35
-rw-r--r--design/sass/_footer.scss54
-rw-r--r--design/sass/_global.scss243
-rw-r--r--design/sass/_header.scss135
-rw-r--r--design/sass/_mixins.scss107
-rw-r--r--design/sass/_queries.scss23
-rw-r--r--design/sass/screenv1.scss7
-rw-r--r--design/templates/admin/buttons.html52
-rw-r--r--design/templates/admin/index.html148
-rw-r--r--design/templates/admin/message.html20
-rw-r--r--design/templates/base.html91
-rw-r--r--design/templates/homepage.html88
-rwxr-xr-xmanage.py18
76 files changed, 3961 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3b61590
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+*.sass-cache
+*~
+.Trashes
+Icon?
+ehthumbs.db
+Thumbs.db
+*.pyc
+venv/
+site/
+/static/
+config/settings.py
+bak/
+/*.json
+.vagrant/
+.env
+*.log
diff --git a/app/blog/__init__.py b/app/blog/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/blog/__init__.py
diff --git a/app/blog/admin.py b/app/blog/admin.py
new file mode 100644
index 0000000..6a4749f
--- /dev/null
+++ b/app/blog/admin.py
@@ -0,0 +1,46 @@
+from django.contrib import admin
+
+from utils.widgets import LGEntryForm
+
+from .models import Entry
+
+
+@admin.register(Entry)
+class EntryAdmin(admin.ModelAdmin):
+ form = LGEntryForm
+ list_display = ('title', 'pub_date', 'enable_comments', 'status')
+ list_filter = ('pub_date', 'enable_comments', 'status')
+ prepopulated_fields = {"slug": ('title',)}
+ fieldsets = (
+ ('Entry', {
+ 'fields': (
+ 'title',
+ 'sub_title',
+ 'body_markdown',
+ ('pub_date', 'status'),
+ 'meta_description',
+ ('slug', 'enable_comments', 'has_code'),
+ ),
+ 'classes': (
+ 'show',
+ 'extrapretty',
+ 'wide'
+ )
+ }),
+ ('meta', {
+ 'fields': (
+ 'originally_published_by',
+ 'originally_published_by_url',
+ 'afterword',
+ ('field_notes', 'books'),
+ ),
+ 'classes': (
+ 'hide',
+ 'extrapretty',
+ 'wide'
+ )
+ }),
+ )
+
+ class Media:
+ js = ('image-loader.js', 'next-prev-links.js')
diff --git a/app/blog/build.py b/app/blog/build.py
new file mode 100644
index 0000000..392e991
--- /dev/null
+++ b/app/blog/build.py
@@ -0,0 +1,22 @@
+import os
+from builder.base import BuildNew
+from django.urls import reverse
+from . import models
+
+
+class BuildEssays(BuildNew):
+
+ def build(self):
+ self.build_list_view()
+ self.build_detail_view()
+ # These are the unique classes for this model:
+ #self.build_feed("src:feed")
+
+ def build_list_view(self):
+ response = self.client.get('/essays/')
+ self.write_file('essays/', response.content)
+
+
+def essaybuilder():
+ j = BuildEssays("essays", "essay")
+ j.build()
diff --git a/app/blog/migrations/0001_initial.py b/app/blog/migrations/0001_initial.py
new file mode 100644
index 0000000..be54329
--- /dev/null
+++ b/app/blog/migrations/0001_initial.py
@@ -0,0 +1,43 @@
+# Generated by Django 2.1.7 on 2019-03-30 17:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('photos', '0001_initial'),
+ ('taxonomy', '__first__'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Entry',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('sub_title', models.CharField(blank=True, max_length=200)),
+ ('dek', models.TextField(blank=True)),
+ ('slug', models.SlugField(unique_for_date='pub_date')),
+ ('body_html', models.TextField(blank=True)),
+ ('body_markdown', models.TextField()),
+ ('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)),
+ ('meta_description', models.CharField(blank=True, max_length=256, null=True)),
+ ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='taxonomy.Category')),
+ ('featured_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='photos.LuxImage')),
+ ('tags', taggit.managers.TaggableManager(blank=True, help_text='Topics Covered', through='taxonomy.TaggedItems', to='taxonomy.LuxTag', verbose_name='Tags')),
+ ],
+ options={
+ 'get_latest_by': 'pub_date',
+ 'verbose_name_plural': 'Essays',
+ 'ordering': ('-pub_date',),
+ },
+ ),
+ ]
diff --git a/app/blog/migrations/__init__.py b/app/blog/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/blog/migrations/__init__.py
diff --git a/app/blog/models.py b/app/blog/models.py
new file mode 100644
index 0000000..3d1ae30
--- /dev/null
+++ b/app/blog/models.py
@@ -0,0 +1,70 @@
+from django.db import models
+from django.urls import reverse
+from django.contrib.sitemaps import Sitemap
+import datetime
+
+from taggit.managers import TaggableManager
+
+from taxonomy.models import TaggedItems, Category
+from utils.util import render_images, markdown_to_html
+from photos.models import LuxImage
+
+
+class Entry(models.Model):
+ title = models.CharField(max_length=200)
+ sub_title = models.CharField(max_length=200, blank=True)
+ dek = models.TextField(blank=True)
+ slug = models.SlugField(unique_for_date='pub_date')
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField()
+ 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)
+ meta_description = models.CharField(max_length=256, null=True, blank=True)
+ tags = TaggableManager(through=TaggedItems, blank=True, help_text='Topics Covered')
+ featured_image = models.ForeignKey(LuxImage, on_delete=models.CASCADE, null=True, blank=True)
+ category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True)
+
+ class Meta:
+ ordering = ('-pub_date',)
+ get_latest_by = 'pub_date'
+ verbose_name_plural = 'Essays'
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse('essays:detail', kwargs={"slug": self.slug})
+
+ def comment_period_open(self):
+ return self.enable_comments and datetime.datetime.today() - datetime.timedelta(30) <= self.pub_date
+
+ @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 = render_images(self.body_markdown)
+ self.body_html = markdown_to_html(md)
+ super(Entry, self).save()
+
+
+class GuideSitemap(Sitemap):
+ changefreq = "never"
+ priority = 1.0
+ protocol = "https"
+
+ def items(self):
+ return Essay.objects.filter(status=1)
+
+ def lastmod(self, obj):
+ return obj.pub_date
diff --git a/app/blog/urls.py b/app/blog/urls.py
new file mode 100644
index 0000000..da3e1fd
--- /dev/null
+++ b/app/blog/urls.py
@@ -0,0 +1,28 @@
+from django.urls import path, re_path
+
+from . import views
+
+app_name = "blog"
+
+urlpatterns = [
+ #path(
+ # r'topic/<str:slug>',
+ # views.TopicListView.as_view(),
+ # name="list_topics"
+ #),
+ path(
+ r'<str:slug>',
+ views.EntryDetailView.as_view(),
+ name="detail"
+ ),
+ path(
+ r'<str:slug>',
+ views.EntryDetailViewTXT.as_view(),
+ name="detail-txt"
+ ),
+ path(
+ r'',
+ views.EntryListView.as_view(),
+ name="list",
+ ),
+]
diff --git a/app/blog/views.py b/app/blog/views.py
new file mode 100644
index 0000000..56bd823
--- /dev/null
+++ b/app/blog/views.py
@@ -0,0 +1,47 @@
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+from django.contrib.syndication.views import Feed
+
+
+from .models import Entry
+
+
+class EntryListView(ListView):
+ model = Entry
+
+ def get_queryset(self, **kwargs):
+ qs = Entry.objects.filter(status=1)
+ return qs
+
+
+class EntryDetailView(DetailView):
+ model = Entry
+
+
+class EntryDetailViewTXT(EntryDetailView):
+ template_name = "entry_detail.txt"
+
+
+'''
+class TopicListView(ListView):
+ template_name = 'archives/src_home.html'
+
+ def queryset(self):
+ return Post.objects.filter(topics__slug=self.kwargs['slug'])
+
+ 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'] = Topic.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 Post.objects.filter(status__exact=1).order_by('-pub_date')[:10]
+'''
diff --git a/app/builder/__init__.py b/app/builder/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/builder/__init__.py
diff --git a/app/builder/base.py b/app/builder/base.py
new file mode 100644
index 0000000..0d2cb0f
--- /dev/null
+++ b/app/builder/base.py
@@ -0,0 +1,152 @@
+import os
+from math import ceil
+from decimal import Decimal
+from django.test.client import Client
+from django.template.loader import render_to_string
+from django.template import Context
+from django.urls import reverse
+from django.apps import apps
+from django.conf import settings
+from jsmin import jsmin
+
+
+class _FileWriter(object):
+ """
+ Given a path and text object; write the page to disc
+ """
+ def __init__(self, path, text_object, ext='html', filename='index', base_path=settings.FLATFILES_ROOT):
+ self.path = '%s%s' % (base_path, path)
+ if not os.path.isdir(self.path):
+ os.makedirs(self.path)
+ fpath = '%s%s.%s' % (self.path, filename, ext)
+ self.write(fpath, text_object)
+
+ def write(self, fpath, text_object):
+ f = open(fpath, 'wb')
+ f.write(text_object)
+ f.close()
+
+ def compress_js(self, filename, text_object):
+ path = '%s%s.min.js' % (self.path, filename)
+ compressed = jsmin(text_object.decode('utf-8')).encode('utf-8')
+ self.write(path, compressed)
+
+
+class BuildNew():
+ def __init__(self, model, app):
+ self.model = apps.get_model(model, app)
+ self.get_model_queryset()
+ self.client = Client()
+
+ def build(self):
+ self.build_list_view()
+ self.build_detail_view()
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(status__exact=1)
+
+ def write_file(self, path, text_object, ext='html', filename='index'):
+ self.writer = _FileWriter(path, text_object, ext=ext, filename=filename)
+
+ def get_pages(self, qs, paginate_by):
+ return int(ceil(Decimal(qs.count()) / Decimal(paginate_by)))
+
+ def build_list_view(self, base_path='', qs=None, paginate_by=10):
+ """
+ Archive Page builder that actually crawls the urls
+ because we need to be able to pass a request object to the template
+ """
+
+ if not qs:
+ qs = self.get_model_queryset()
+ pages = self.get_pages(qs, paginate_by)
+ for page in range(pages):
+ if int(pages) > 1:
+ path = '%s%s/' % (base_path, str(page + 1))
+ url = '%s%s/' % (base_path, str(page + 1))
+ else:
+ path = base_path
+ url = base_path
+ print(path)
+ response = self.client.get(url, HTTP_HOST='127.0.0.1', follow=True)
+ if page == 0:
+ self.write_file(base_path, response.content)
+ self.write_file(path, response.content)
+
+ def build_year_view(self, url, paginate_by=99999):
+ years = self.model.objects.dates('pub_date', 'year')
+ for year in years:
+ year = year.strftime('%Y')
+ qs = self.model.objects.filter(
+ status__exact=1,
+ pub_date__year=year
+ )
+ self.build_list_view(
+ base_path=reverse(url, kwargs={'year': year, }),
+ qs=qs,
+ paginate_by=paginate_by
+ )
+
+ def build_month_view(self, url, paginate_by=99999):
+ months = self.model.objects.dates('pub_date', 'month')
+ for m in months:
+ year = m.strftime('%Y')
+ month = m.strftime('%m')
+ qs = self.model.objects.filter(
+ status__exact=1,
+ pub_date__year=year,
+ pub_date__month=month
+ )
+ if qs.exists():
+ self.build_list_view(
+ base_path=reverse(url, kwargs={'year': year, 'month': month}),
+ qs=qs,
+ paginate_by=paginate_by
+ )
+
+ def build_detail_view(self):
+ '''
+ Grab all the blog posts, render them to a template
+ string and write that out to the filesystem
+ '''
+ for entry in self.get_model_queryset():
+ url = entry.get_absolute_url()
+ path, slug = os.path.split(entry.get_absolute_url())
+ path = '%s/' % path
+ # write html
+ response = self.client.get(url)
+ self.write_file(path, response.content, filename=slug)
+ # write txt
+ response = self.client.get('%s.txt' % url)
+ self.write_file(path, response.content, ext='txt', filename=slug)
+
+
+ def build_feed(self, url_name):
+ """
+ Not called, but available for subclassing
+ """
+ url = reverse(url_name,)
+ path, slug = os.path.split(url)
+ slug, ext = os.path.splitext(slug)
+ response = self.client.get(url, HTTP_HOST='127.0.0.1')
+ self.write_file('%s/' % path, response.content, ext=ext.split(".")[-1], filename=slug)
+
+
+class BuildSitemap(BuildNew):
+ def build(self):
+ c = Client()
+ response = c.get('/sitemap.xml', HTTP_HOST='127.0.0.1')
+ self.write_file('', response.content, 'xml', 'sitemap')
+
+
+class BuildPages(BuildNew):
+ def build(self):
+ model = apps.get_model('pages', 'page')
+ pages = model.objects.all()
+ for page in pages:
+ c = Context({'object':page,'SITE_URL':settings.SITE_URL, 'MEDIA_URL':settings.BAKED_MEDIA_URL})
+ t = render_to_string(["details/%s.html" % page.slug, 'details/page.html'],c).encode('utf-8')
+ s = render_to_string('details/page.txt',c).encode('utf-8')
+ fpath = '%s' %(page.slug)
+ self.write_file('', t, 'html', page.slug)
+ self.write_file('', t, 'txt', page.slug)
diff --git a/app/builder/sanitizer.py b/app/builder/sanitizer.py
new file mode 100644
index 0000000..8512f4f
--- /dev/null
+++ b/app/builder/sanitizer.py
@@ -0,0 +1,60 @@
+from bs4 import BeautifulSoup
+
+
+class Sanitizer(object):
+ blacklisted_tags = []
+ blacklisted_attributes = []
+ blacklisted_protocols = []
+
+ def __init__(self, tags=None, attributes=None, protocols=None):
+ if tags:
+ self.blacklisted_tags = tags
+ if attributes:
+ self.blacklisted_attributes = attributes
+ if protocols:
+ self.blacklisted_protocols = protocols
+
+ def strip(self, content=None):
+ """Strip HTML content to meet standards of output type.
+ Meant to be subclassed for each converter.
+
+ Keyword arguments:
+ content -- subset of an HTML document. (ie. contents of a body tag)
+ """
+ if not content:
+ content = self.content
+ return content
+
+ soup = BeautifulSoup(content, "lxml")
+ self.strip_tags(soup)
+ self.strip_attributes(soup)
+
+ output = soup.body.decode_contents()
+ return output
+
+ def strip_tags(self, soup):
+ if self.blacklisted_tags:
+ [x.extract() for x in soup.find_all(self.blacklisted_tags)]
+
+ def strip_attributes_extra(self, node):
+ pass
+
+ def strip_attributes(self, soup):
+ if not (self.blacklisted_attributes or self.blacklisted_protocols):
+ return
+
+ for node in soup.body.find_all(True):
+ attributes = node.attrs.keys()
+ if not attributes:
+ continue
+
+ for attr in self.blacklisted_attributes:
+ if attr in attributes:
+ del node.attrs[attr]
+
+ self.strip_attributes_extra(node)
+
+ if 'href' in attributes:
+ protocol = node['href'].split(':')[0]
+ if protocol in self.blacklisted_protocols:
+ del node['href'] \ No newline at end of file
diff --git a/app/builder/views.py b/app/builder/views.py
new file mode 100644
index 0000000..9d12aaa
--- /dev/null
+++ b/app/builder/views.py
@@ -0,0 +1,13 @@
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+#from src.build import builder as src_builder
+from pages.build import builder as page_builder
+
+
+def do_build(request):
+ section = request.GET.get('id', '')
+ context = {}
+ if section == 'pages':
+ context = {'message': 'Writing Pages to Disk'}
+ page_builder()
+ return render_to_response('admin/message.html', context)
diff --git a/app/pages/__init__.py b/app/pages/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/pages/__init__.py
diff --git a/app/pages/admin.py b/app/pages/admin.py
new file mode 100644
index 0000000..706d690
--- /dev/null
+++ b/app/pages/admin.py
@@ -0,0 +1,32 @@
+from django.contrib import admin
+
+from django import forms
+
+from pages.models import Page
+
+
+class PageEntryForm(forms.ModelForm):
+ class Meta:
+ model = Page
+ fields = '__all__'
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 50, 'cols': 100}),
+ }
+
+
+@admin.register(Page)
+class PageAdmin(admin.ModelAdmin):
+ form = PageEntryForm
+ list_display = ('title', 'slug', 'path')
+ search_fields = ['title', 'body_markdown']
+ prepopulated_fields = {"slug": ('title',)}
+ fieldsets = (
+ ('Page', {
+ 'fields': ('title', 'body_markdown', ('slug', 'path', )),
+ 'classes': ('show', 'extrapretty', 'wide')
+ }),
+ ('Metadata', {
+ 'classes': ('collapse closed',),
+ 'fields': ('meta_description',),
+ })
+ )
diff --git a/app/pages/build.py b/app/pages/build.py
new file mode 100644
index 0000000..75dbd0d
--- /dev/null
+++ b/app/pages/build.py
@@ -0,0 +1,39 @@
+import os
+from django.template.loader import render_to_string
+from django.template import Context
+from django.urls import reverse
+from django.conf import settings
+
+from builder.base import BuildNew
+
+
+class BuildPages(BuildNew):
+ def build(self):
+ self.build_detail_view()
+ print("building pages")
+
+ def build_detail_view(self):
+ '''
+ Grab all the blog posts, render them to a template
+ string and write that out to the filesystem
+ '''
+ for entry in self.get_model_queryset():
+ url = entry.get_absolute_url()
+ path, slug = os.path.split(entry.get_absolute_url())
+ path = '%s/' % path
+ # write html
+ response = self.client.get(url)
+ if slug == 'homepage':
+ slug = 'index'
+ self.write_file(path, response.content, filename=slug)
+ # write txt
+ response = self.client.get('%s.txt' % url)
+ self.write_file(path, response.content, ext='txt', filename=slug)
+
+ def get_model_queryset(self):
+ return self.model.objects.filter(build=True)
+
+
+def builder():
+ j = BuildPages("pages", "Page")
+ j.build()
diff --git a/app/pages/migrations/0001_initial.py b/app/pages/migrations/0001_initial.py
new file mode 100644
index 0000000..49e763f
--- /dev/null
+++ b/app/pages/migrations/0001_initial.py
@@ -0,0 +1,26 @@
+# Generated by Django 2.1.2 on 2018-11-11 21:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Page',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200)),
+ ('slug', models.SlugField()),
+ ('body_html', models.TextField(blank=True)),
+ ('body_markdown', models.TextField()),
+ ('meta_description', models.CharField(blank=True, max_length=256, null=True)),
+ ('path', models.CharField(blank=True, max_length=200, null=True)),
+ ],
+ ),
+ ]
diff --git a/app/pages/migrations/0002_page_build.py b/app/pages/migrations/0002_page_build.py
new file mode 100644
index 0000000..6dd9fc0
--- /dev/null
+++ b/app/pages/migrations/0002_page_build.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.7 on 2019-04-11 23:22
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pages', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='page',
+ name='build',
+ field=models.BooleanField(default=True),
+ ),
+ ]
diff --git a/app/pages/migrations/__init__.py b/app/pages/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/pages/migrations/__init__.py
diff --git a/app/pages/models.py b/app/pages/models.py
new file mode 100644
index 0000000..1c9ab23
--- /dev/null
+++ b/app/pages/models.py
@@ -0,0 +1,42 @@
+from django.db import models
+from django.template.defaultfilters import slugify
+from django.contrib.sitemaps import Sitemap
+
+from utils.util import markdown_to_html, render_images
+
+
+class Page(models.Model):
+ title = models.CharField(max_length=200)
+ slug = models.SlugField()
+ body_html = models.TextField(blank=True)
+ body_markdown = models.TextField()
+ meta_description = models.CharField(max_length=256, null=True, blank=True)
+ path = models.CharField(max_length=200, null=True, blank=True)
+ build = models.BooleanField(default=True)
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ if self.path:
+ return "/%s/%s" % (self.path, self.slug)
+ else:
+ return "/%s" % (self.slug)
+
+ def save(self):
+ # run markdown
+ md = render_images(self.body_markdown)
+ self.body_html = markdown_to_html(md)
+ if not self.id:
+ # self.date_created = timezone.now()
+ self.slug = slugify(self.title)[:50]
+ super(Page, self).save()
+
+
+class PageSitemap(Sitemap):
+ changefreq = "never"
+ priority = 1.0
+ protocol = "https"
+
+ def items(self):
+ return Page.objects.all()
diff --git a/app/pages/tests/__init__.py b/app/pages/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/pages/tests/__init__.py
diff --git a/app/pages/tests/test_models.py b/app/pages/tests/test_models.py
new file mode 100644
index 0000000..2722430
--- /dev/null
+++ b/app/pages/tests/test_models.py
@@ -0,0 +1,36 @@
+from django.test import TestCase
+
+from pages.models import Page
+
+
+class PageModelTest(TestCase):
+ def setUp(self):
+ self.page = Page(
+ title="Test Page",
+ meta_description="The meta desc",
+ body_markdown="the body of the page",
+ )
+ self.page.save()
+ self.pathpage = Page(
+ title="Test Page",
+ meta_description="The meta desc",
+ body_markdown="the body of the page",
+ path="test-path",
+ )
+ self.pathpage.save()
+
+ def test_string_representation(self):
+ self.assertEqual(str(self.page), "Test Page")
+ self.assertEqual(str(self.page.slug), "test-page")
+ self.assertEqual(str(self.page.body_markdown), "the body of the page")
+ self.assertEqual(str(self.page.body_html), "<p>the body of the page</p>")
+ self.assertEqual(str(self.page.meta_description), "The meta desc")
+ self.assertEqual(self.page.path, None)
+
+ def test_get_absolute_url(self):
+ """Absolute URL should return /page """
+ self.assertEqual(str(self.page.get_absolute_url()), "/test-page")
+
+ def test_path_get_absolute_url(self):
+ """Absolute URL with a path should return /path/page """
+ self.assertEqual(str(self.pathpage.get_absolute_url()), "/test-path/test-page")
diff --git a/app/pages/tests/test_views.py b/app/pages/tests/test_views.py
new file mode 100644
index 0000000..c771a29
--- /dev/null
+++ b/app/pages/tests/test_views.py
@@ -0,0 +1,27 @@
+from django.test import RequestFactory, TestCase
+from django.contrib import auth
+
+from pages.models import Page
+
+User = auth.get_user_model()
+
+
+class PageViewTest(TestCase):
+ def setUp(self):
+ # Every test needs access to the request factory.
+ self.factory = RequestFactory()
+ self.page = Page(
+ title="Test Page",
+ meta_description="The meta desc",
+ body_markdown="the body of the page",
+ )
+ self.page.save()
+
+ def test_non_existent_page(self):
+ """A non-existent staticflatpage raises a 404."""
+ response = self.client.get('/no_such_page/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_detail_view(self):
+ response = self.client.get(self.page.get_absolute_url())
+ self.assertEqual(response.status_code, 200)
diff --git a/app/pages/views.py b/app/pages/views.py
new file mode 100644
index 0000000..41288dd
--- /dev/null
+++ b/app/pages/views.py
@@ -0,0 +1,18 @@
+from django.views.generic.detail import DetailView
+from django.contrib.auth.forms import AuthenticationForm
+from pages.models import Page
+
+
+class PageDetailView(DetailView):
+ model = Page
+ slug_field = "slug"
+
+ def get_template_names(self):
+ obj = self.get_object()
+ return ["%s.html" % obj.slug, "pages/%s.html" % obj.slug, 'pages/page.html']
+
+
+class HomePageDetailView(PageDetailView):
+
+ def get_object(self):
+ return Page.objects.get(slug='homepage')
diff --git a/app/photos/__init__.py b/app/photos/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/photos/__init__.py
diff --git a/app/photos/admin.py b/app/photos/admin.py
new file mode 100644
index 0000000..072808c
--- /dev/null
+++ b/app/photos/admin.py
@@ -0,0 +1,32 @@
+from django.contrib import admin
+from photos.models import LuxImage, LuxImageSize, LuxVideo
+
+
+@admin.register(LuxImageSize)
+class LuxImageSizeAdmin(admin.ModelAdmin):
+ list_display = ('name', 'width', 'height', 'quality')
+
+
+@admin.register(LuxVideo)
+class LuxVideoAdmin(admin.ModelAdmin):
+ pass
+
+
+@admin.register(LuxImage)
+class LuxImageAdmin(admin.ModelAdmin):
+ list_display = ('pk', 'admin_thumbnail', 'pub_date', 'caption')
+ list_filter = ('pub_date',)
+ search_fields = ['title', 'caption']
+
+ fieldsets = (
+ (None, {
+ 'fields': ('title', ('image'), 'pub_date', 'sizes', 'alt', 'caption', ('is_public'), ('photo_credit_source', 'photo_credit_url'))
+ }),
+ ('Exif Data', {
+ 'classes': ('collapse',),
+ 'fields': ('height', 'width'),
+ }),
+ )
+
+ class Media:
+ js = ('image-preview.js', 'next-prev-links.js')
diff --git a/app/photos/detail_urls.py b/app/photos/detail_urls.py
new file mode 100644
index 0000000..0ab94f6
--- /dev/null
+++ b/app/photos/detail_urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+from django.views.generic.detail import DetailView
+from photos.models import Photo
+
+urlpatterns = [
+ url(
+ r'^(?P<object_id>\d+)/$',
+ DetailView.as_view(model=Photo, template_name='details/photo.html')
+ ),
+]
diff --git a/app/photos/forms.py b/app/photos/forms.py
new file mode 100644
index 0000000..126cfaf
--- /dev/null
+++ b/app/photos/forms.py
@@ -0,0 +1,173 @@
+import zipfile
+from zipfile import BadZipFile
+import logging
+import datetime
+import os
+from io import BytesIO
+try:
+ import Image
+except ImportError:
+ from PIL import Image
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from django.contrib import messages
+from django.core.files.base import ContentFile
+from django.contrib.admin import widgets
+from django.utils.safestring import mark_safe
+
+from photos.models import LuxImage, LuxGallery, LuxImageSize
+
+logger = logging.getLogger('photos.forms')
+
+
+class GalleryForm(forms.ModelForm):
+ class Meta:
+ fields = '__all__'
+ widgets = {
+ 'images': forms.SelectMultiple,
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(GalleryForm, self).__init__(*args, **kwargs)
+ self.fields['images'].choices = [(image.id, mark_safe('%sqq%sqq%s' % (image.title, image.get_image_by_size('tn'), image.pk))) for image in LuxImage.objects.all()[:40]]
+ self.fields['images'].allow_tags = True
+
+
+class ImageChoiceField(forms.ModelMultipleChoiceField):
+
+ def label_from_instance(self, obj):
+
+ return mark_safe('%sqq%sqq%s' % (obj.title, obj.get_image_by_size('tn'), obj.pk))
+
+
+class FKGalleryForm(forms.ModelForm):
+ class Meta:
+ fields = '__all__'
+ widgets = {
+ 'image': ImageChoiceField(queryset=LuxImage.objects.all()),
+ }
+
+ def __init__(self, *args, **kwargs):
+ super(FKGalleryForm, self).__init__(*args, **kwargs)
+ self.fields['image'].choices = [(o.id, str(o.image.url)) for o in LuxImage.objects.all()]
+ self.fields['image'].allow_tags = True
+
+
+class UploadZipForm(forms.Form):
+ """
+ Handles the uploading of a gallery of photos packed in a .zip file
+ Creates Gallery object, adds photos with all metadata that's available
+ """
+ zip_file = forms.FileField()
+ title = forms.CharField(label=_('Gallery Title'), max_length=250)
+ slug = forms.SlugField(label=_('Gallery Slug'))
+ desc = forms.CharField(label=_('Gallery Caption'), widget=forms.Textarea, required=False)
+ date = forms.SplitDateTimeField(label=_('Date'), widget=widgets.AdminSplitDateTime)
+ is_public = forms.BooleanField(label=_('Is public'), initial=True, required=False, help_text=_('Show on site'))
+
+ def clean_zip_file(self):
+ """Open the zip file a first time, to check that it is a valid zip archive.
+ We'll open it again in a moment, so we have some duplication, but let's focus
+ on keeping the code easier to read!
+ """
+ zip_file = self.cleaned_data['zip_file']
+ try:
+ zip = zipfile.ZipFile(zip_file)
+ except BadZipFile as e:
+ raise forms.ValidationError(str(e))
+ bad_file = zip.testzip()
+ if bad_file:
+ zip.close()
+ raise forms.ValidationError('"%s" in the .zip archive is corrupt.' % bad_file)
+ zip.close() # Close file in all cases.
+ return zip_file
+
+ def clean_title(self):
+ title = self.cleaned_data['title']
+ if title and LuxGallery.objects.filter(title=title).exists():
+ raise forms.ValidationError(_('A gallery with that title already exists.'))
+ return title
+
+ def clean(self):
+ cleaned_data = super(UploadZipForm, self).clean()
+ if not self['title'].errors:
+ # If there's already an error in the title, no need to add another
+ # error related to the same field.
+ if not cleaned_data.get('title', None) and not cleaned_data['gallery']:
+ raise forms.ValidationError(
+ _('Select an existing gallery, or enter a title for a new gallery.'))
+ return cleaned_data
+
+ def save(self, request=None, zip_file=None):
+ if not zip_file:
+ zip_file = self.cleaned_data['zip_file']
+
+ gallery, created = LuxGallery.objects.get_or_create(
+ title=self.cleaned_data['title'],
+ description=self.cleaned_data['desc'],
+ slug=self.cleaned_data['slug'],
+ pub_date=self.cleaned_data['date'],
+ is_public=self.cleaned_data['is_public']
+ )
+ zipper = zipfile.ZipFile(zip_file)
+ count = 1
+ for filename in sorted(zipper.namelist()):
+ f, file_extension = os.path.splitext(filename)
+ logger.debug('Reading file "{0}".'.format(filename))
+ if filename.startswith('__') or filename.startswith('.'):
+ logger.debug('Ignoring file "{0}".'.format(filename))
+ continue
+ if os.path.dirname(filename):
+ logger.warning('Ignoring file "{0}" as it is in a subfolder; all images should be in the top '
+ 'folder of the zip.'.format(filename))
+ if request:
+ messages.warning(request,
+ _('Ignoring file "{filename}" as it is in a subfolder').format(filename=filename), fail_silently=True)
+ continue
+ data = zipper.read(filename)
+
+ if not len(data):
+ logger.debug('File "{0}" is empty.'.format(filename))
+ continue
+
+ fn, file_extension = os.path.splitext(filename)
+ if file_extension != ".mp4":
+ # Basic check that we have a valid image.
+ try:
+ file = BytesIO(data)
+ opened = Image.open(file)
+ opened.verify()
+ except Exception:
+ # Pillow (or PIL) doesn't recognize it as an image.
+ # If a "bad" file is found we just skip it.
+ # But we do flag this both in the logs and to the user.
+ logger.error('Could not process file "{0}" in the .zip archive.'.format(filename))
+ if request:
+ messages.warning(request,
+ _('Could not process file "{0}" in the .zip archive.').format(
+ filename),
+ fail_silently=True)
+ continue
+ image = LuxImage(
+ pub_date=datetime.datetime.now()
+ )
+ contentfile = ContentFile(data)
+ image.image.save(filename, contentfile)
+ if file_extension != ".mp4":
+ img = Image.open(image.image.path)
+ if img.size[0] > img.size[1]:
+ image.sizes.add(LuxImageSize.objects.get(width=2560))
+ image.sizes.add(LuxImageSize.objects.get(width=1170))
+ image.sizes.add(LuxImageSize.objects.get(width=720))
+ if img.size[1] > img.size[0]:
+ image.sizes.add(LuxImageSize.objects.get(height=1600))
+ image.sizes.add(LuxImageSize.objects.get(height=800))
+ image.sizes.add(LuxImageSize.objects.get(height=460))
+ image.save()
+ gallery.images.add(image)
+
+ zipper.close()
+
+ if request:
+ messages.success(request, _('The photos have been uploaded'))
diff --git a/app/photos/migrations/0001_initial.py b/app/photos/migrations/0001_initial.py
new file mode 100644
index 0000000..f06bad5
--- /dev/null
+++ b/app/photos/migrations/0001_initial.py
@@ -0,0 +1,73 @@
+# Generated by Django 2.1.7 on 2019-03-30 17:07
+
+import datetime
+from django.db import migrations, models
+import photos.models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LuxImage',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('image', models.FileField(blank=True, null=True, upload_to=photos.models.get_upload_path)),
+ ('title', models.CharField(blank=True, max_length=300, null=True)),
+ ('alt', models.CharField(blank=True, max_length=300, null=True)),
+ ('photo_credit_source', models.CharField(blank=True, max_length=300, null=True)),
+ ('photo_credit_url', models.CharField(blank=True, max_length=300, null=True)),
+ ('caption', models.TextField(blank=True, null=True)),
+ ('pub_date', models.DateTimeField(default=datetime.datetime.now)),
+ ('height', models.CharField(blank=True, max_length=6, null=True)),
+ ('width', models.CharField(blank=True, max_length=6, null=True)),
+ ('is_public', models.BooleanField(default=True)),
+ ],
+ options={
+ 'get_latest_by': 'pub_date',
+ 'ordering': ('-pub_date', 'id'),
+ 'verbose_name_plural': 'Images',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxImageSize',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(blank=True, max_length=30, null=True)),
+ ('width', models.IntegerField(blank=True, null=True)),
+ ('height', models.IntegerField(blank=True, null=True)),
+ ('quality', models.IntegerField()),
+ ],
+ options={
+ 'verbose_name_plural': 'Image Sizes',
+ },
+ ),
+ migrations.CreateModel(
+ name='LuxVideo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('video_mp4', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)),
+ ('video_webm', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)),
+ ('video_poster', models.FileField(blank=True, null=True, upload_to=photos.models.get_vid_upload_path)),
+ ('title', models.CharField(blank=True, max_length=300, null=True)),
+ ('pub_date', models.DateTimeField(default=datetime.datetime.now)),
+ ('youtube_url', models.CharField(blank=True, max_length=80, null=True)),
+ ('vimeo_url', models.CharField(blank=True, max_length=300, null=True)),
+ ],
+ options={
+ 'get_latest_by': 'pub_date',
+ 'ordering': ('-pub_date', 'id'),
+ 'verbose_name_plural': 'Videos',
+ },
+ ),
+ migrations.AddField(
+ model_name='luximage',
+ name='sizes',
+ field=models.ManyToManyField(blank=True, to='photos.LuxImageSize'),
+ ),
+ ]
diff --git a/app/photos/migrations/__init__.py b/app/photos/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/photos/migrations/__init__.py
diff --git a/app/photos/models.py b/app/photos/models.py
new file mode 100644
index 0000000..4a1bf99
--- /dev/null
+++ b/app/photos/models.py
@@ -0,0 +1,223 @@
+import os.path
+import io
+import datetime
+from PIL import Image
+
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.contrib.sitemaps import Sitemap
+from django.utils.encoding import force_text
+from django.urls import reverse
+from django.apps import apps
+from django.utils.html import format_html
+from django.utils.text import slugify
+from django.conf import settings
+from django import forms
+
+from resizeimage.imageexceptions import ImageSizeError
+
+from .utils import resize_image
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+from django.db.models.signals import m2m_changed
+
+
+def get_upload_path(self, filename):
+ return "images/original/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+def get_vid_upload_path(self, filename):
+ return "images/videos/%s/%s" % (datetime.datetime.today().strftime("%Y"), filename)
+
+
+class LuxImageSize(models.Model):
+ name = models.CharField(null=True, blank=True, max_length=30)
+ width = models.IntegerField(null=True, blank=True)
+ height = models.IntegerField(null=True, blank=True)
+ quality = models.IntegerField()
+
+ class Meta:
+ verbose_name_plural = 'Image Sizes'
+
+ def __str__(self):
+ if self.width:
+ size = self.width
+ if self.height:
+ size = self.height
+ return "%s - %s" %(self.name, str(size))
+
+
+class LuxImage(models.Model):
+ image = models.FileField(blank=True, null=True, upload_to=get_upload_path)
+ title = models.CharField(null=True, blank=True, max_length=300)
+ alt = models.CharField(null=True, blank=True, max_length=300)
+ photo_credit_source = models.CharField(null=True, blank=True, max_length=300)
+ photo_credit_url = models.CharField(null=True, blank=True, max_length=300)
+ caption = models.TextField(blank=True, null=True)
+ pub_date = models.DateTimeField(default=datetime.datetime.now)
+ height = models.CharField(max_length=6, blank=True, null=True)
+ width = models.CharField(max_length=6, blank=True, null=True)
+ is_public = models.BooleanField(default=True)
+ sizes = models.ManyToManyField(LuxImageSize, blank=True)
+
+ class Meta:
+ ordering = ('-pub_date', 'id')
+ verbose_name_plural = 'Images'
+ get_latest_by = 'pub_date'
+
+ def __str__(self):
+ if self.title:
+ return "%s" % self.title
+ else:
+ return "%s" % self.pk
+
+ def get_type(self):
+ return str(self.__class__.__name__)
+
+ def get_admin_image(self):
+ for size in self.sizes.all():
+ if size.width and size.width <= 820 or size.height and size.height <= 800:
+ return self.get_image_by_size(size.name)
+
+ def get_admin_insert(self):
+ return "/media/images/%s/%s_tn.%s" % (self.pub_date.strftime("%Y"), self.get_image_name(), self.get_image_ext())
+
+ def get_largest_image(self):
+ t = []
+ for size in self.sizes.all():
+ t.append(size.width)
+ t.sort(key=float)
+ t.reverse()
+ return self.get_image_path_by_size(t[0])
+
+ def get_image_name(self):
+ return self.image.url.split("original/")[1][5:-4]
+
+ def get_image_ext(self):
+ return self.image.url[-3:]
+
+ def get_image_by_size(self, size="original"):
+ base = self.get_image_name()
+ if size == "admin_insert":
+ return "images/%s/%s.%s" % (self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ if size == "original":
+ return "%soriginal/%s/%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ else:
+ if size != 'tn':
+ s = LuxImageSize.objects.get(name=size)
+ if s not in self.sizes.all():
+ print("new size is "+s.name)
+ self.sizes.add(s)
+ return "%s%s/%s_%s.%s" % (settings.IMAGES_URL, self.pub_date.strftime("%Y"), base, size, self.get_image_ext())
+
+ def get_image_path_by_size(self, size="original"):
+ base = self.get_image_name()
+ if size == "original":
+ return "%s/original/%s/%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, self.get_image_ext())
+ else:
+ return "%s/%s/%s_%s.%s" % (settings.IMAGES_ROOT, self.pub_date.strftime("%Y"), base, size, self.get_image_ext())
+
+ def admin_thumbnail(self):
+ return format_html('<a href="%s"><img src="%s"></a>' % (self.get_image_by_size(), self.get_image_by_size("tn")))
+ admin_thumbnail.short_description = 'Thumbnail'
+
+ @property
+ def get_previous_published(self):
+ return self.get_previous_by_pub_date()
+
+ @property
+ def get_next_published(self):
+ return self.get_next_by_pub_date()
+
+ @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_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 is_portait(self):
+ if int(self.height) > int(self.width):
+ return True
+ else:
+ return False
+
+ def save(self, *args, **kwargs):
+ super(LuxImage, self).save()
+
+
+@receiver(post_save, sender=LuxImage)
+def post_save_events(sender, update_fields, created, instance, **kwargs):
+ if instance.exif_raw == '':
+ filename, file_extension = os.path.splitext(instance.image.path)
+ if file_extension != ".mp4":
+ img = Image.open(instance.image.path)
+ instance.height = img.height
+ instance.width = img.width
+ #instance = readexif(instance)
+ post_save.disconnect(post_save_events, sender=LuxImage)
+ instance.save()
+ post_save.connect(post_save_events, sender=LuxImage)
+
+
+@receiver(m2m_changed, sender=LuxImage.sizes.through)
+def update_photo_sizes(sender, instance, **kwargs):
+ base_path = "%s/%s/" % (settings.IMAGES_ROOT, instance.pub_date.strftime("%Y"))
+ filename, file_extension = os.path.splitext(instance.image.path)
+ if file_extension != ".mp4":
+ img = Image.open(instance.image.path)
+ resize_image(img, 160, None, 78, base_path, "%s_tn.%s" % (instance.get_image_name(), instance.get_image_ext()))
+ for size in instance.sizes.all():
+ if size.width:
+ print("Image width is:"+str(img.width))
+ try:
+ if size.width <= img.width:
+ resize_image(img, size.width, None, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext()))
+ else:
+ raise ValidationError({'items': ["Size is larger than source image"]})
+ except ImageSizeError:
+ m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through)
+ instance.sizes.remove(size)
+ m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through)
+ if size.height:
+ try:
+ if size.height <= img.height:
+ resize_image(img, None, size.height, size.quality, base_path, "%s_%s.%s" % (instance.get_image_name(), slugify(size.name), instance.get_image_ext()))
+
+ else:
+ pass
+ except ImageSizeError:
+ m2m_changed.disconnect(update_photo_sizes, sender=LuxImage.sizes.through)
+ instance.sizes.remove(size)
+ m2m_changed.connect(update_photo_sizes, sender=LuxImage.sizes.through)
+
+
+class LuxVideo(models.Model):
+ video_mp4 = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ video_webm = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ video_poster = models.FileField(blank=True, null=True, upload_to=get_vid_upload_path)
+ title = models.CharField(null=True, blank=True, max_length=300)
+ pub_date = models.DateTimeField(default=datetime.datetime.now)
+ youtube_url = models.CharField(null=True, blank=True, max_length=80)
+ vimeo_url = models.CharField(null=True, blank=True, max_length=300)
+
+ def __str__(self):
+ if self.title:
+ return self.title
+ else:
+ return str(self.pk)
+
+ def get_type(self):
+ return str(self.__class__.__name__)
+
+ class Meta:
+ ordering = ('-pub_date', 'id')
+ verbose_name_plural = 'Videos'
+ get_latest_by = 'pub_date'
diff --git a/app/photos/photos.js b/app/photos/photos.js
new file mode 100644
index 0000000..b93467a
--- /dev/null
+++ b/app/photos/photos.js
@@ -0,0 +1,71 @@
+//Utility functions for map info window
+function mapit(obj) {
+ lat = parseFloat(obj.attr('data-latitude'));
+ lon = parseFloat(obj.attr('data-longitude'));
+ elid= obj.attr('data-imgid');
+ map = L.map(document.getElementById("mw-"+elid));
+ centerCoord = new L.LatLng(lat, lon);
+ zoom = 8;
+ L.tileLayer.provider('Esri.WorldTopoMap', {maxZoom: 18, attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Tiles &copy; Esri and the GIS User Community'}).addTo(map);
+ map.setView(centerCoord, zoom);
+ L.marker([lat, lon]).addTo(map);
+}
+ //########## utility functions to create/remove map container ############
+function create_map(obj) {
+ //find id of this image caption:
+ var imgid = obj.attr('data-imgid');
+ //create container divs
+ $('<div class="map-container" id="mc-'+imgid+'">').insertBefore($(obj).parent().parent());
+ //$(obj).parent().parent().parent().prepend('<div class="map-container" id="mc-'+imgid+'">');
+ $('#mc-'+imgid).append('<div class="map-wrapper" id="mw-'+imgid+'">');
+ //deal with the variable height of div.legend
+ $('#mc-'+imgid).css({
+ bottom: function(index, value) {
+ return parseFloat($(obj).parent().parent().height())+20;
+ }
+ });
+
+ mapit(obj);
+}
+function remove_map(imgid) {
+ $('#mc-'+imgid).remove();
+}
+
+//############ Document.ready events ##############
+$(document).ready(function(){
+
+ //set up click events for map button
+ $('.map-link').click( function() {
+ imgid = $(this).attr('data-imgid');
+ if ($('#mc-'+imgid).is(":visible")) {
+ remove_map(imgid);
+ } else {
+ create_map($(this));
+ }
+ return false;
+
+ });
+ var $ele = $('#slides').children();
+ var $curr = 0;
+ $(document).bind('keydown', function (e) {
+ var code = e.which;
+ switch (code) {
+ case 39:
+ if ($curr <= $ele.size()) {
+ $.scrollTo($ele[$curr], 800 );
+ $curr++;
+ }
+ break;
+ case 37:
+ if ($curr > 0) {
+ $curr--;
+ var $now = $curr;
+ $now--;
+ $.scrollTo($ele[$now], 800 );
+ }
+ break;
+ }
+ return;
+ });
+});
+
diff --git a/app/photos/retriever.py.bak b/app/photos/retriever.py.bak
new file mode 100644
index 0000000..d3c572a
--- /dev/null
+++ b/app/photos/retriever.py.bak
@@ -0,0 +1,314 @@
+from __future__ import division
+import datetime
+import os
+import cStringIO
+import urllib
+
+from django.template.defaultfilters import slugify
+from django.core.exceptions import ObjectDoesNotExist
+from django.utils.encoding import force_unicode
+from django.conf import settings
+
+# Required PIL classes may or may not be available from the root namespace
+# depending on the installation
+try:
+ import Image
+ import ImageFile
+except ImportError:
+ try:
+ from PIL import Image
+ from PIL import ImageFile
+ except ImportError:
+ raise ImportError("Could not import the Python Imaging Library.")
+
+ImageFile.MAXBLOCK = 1000000
+
+from photos.models import Photo, PhotoGallery
+
+# from https://github.com/alexis-mignon/python-flickr-api
+# terribly documented, but offers a good clean OOP approach if you're willing to figure it out...
+import flickr_api
+
+EXIF_PARAMS = {
+ "FNumber": 'f/2.8',
+ "Make": 'Apple',
+ "Model": 'iPhone',
+ "ExposureTime": '',
+ "ISO": '',
+ "FocalLength": '',
+ "LensModel": '',
+ 'DateTimeOriginal': '2013:09:03 22:44:25'
+}
+
+
+def sync_flickr_photos(*args, **kwargs):
+ flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET)
+ flickr_api.set_auth_handler("app/photos/flickrauth")
+ user = flickr_api.test.login()
+ photos = user.getPhotos(extras="date_upload,date_taken,geo")
+ # reverse! reverse!
+ photos.reverse()
+ for photo in photos:
+ info = photo.getInfo()
+ try:
+ row = Photo.objects.get(flickr_id=info['id'], flickr_secret=info['secret'])
+ print('already have ' + info['id'] + ' moving on')
+ except ObjectDoesNotExist:
+ get_photo(photo)
+
+
+def get_photo(photo):
+ info = photo.getInfo()
+ geo = photo.getLocation()
+ location, region = get_geo(float(geo['latitude']), float(geo['longitude']))
+ exif = exif_handler(photo.getExif())
+ p, created = Photo.objects.get_or_create(
+ title=info['title'],
+ flickr_id=info['id'],
+ flickr_owner=info['owner']['id'],
+ flickr_server=info['server'],
+ flickr_secret=info['secret'],
+ flickr_originalsecret=info['originalsecret'],
+ flickr_farm=info['farm'],
+ pub_date=flickr_datetime_to_datetime(info['taken']),
+ description=info['description'],
+ exif_aperture=exif['FNumber'],
+ exif_make=exif['Make'],
+ exif_model=exif['Model'],
+ exif_exposure=exif['ExposureTime'],
+ exif_iso=exif['ISO'],
+ exif_lens=exif['LensModel'],
+ exif_focal_length=exif['FocalLength'],
+ exif_date=flickr_datetime_to_datetime(exif["DateTimeOriginal"].replace(':', '-', 2)),
+ lat=float(geo['latitude']),
+ lon=float(geo['longitude']),
+ region=region,
+ location=location,
+ )
+ if created:
+ for tag in info['tags']:
+ p.tags.add(tag['raw'])
+ p.save()
+ make_local_copies(p)
+ #retina image:
+ #slideshow_image(p, 2000, 1600, 75)
+ #normal image
+ print(p.title)
+ return p
+
+
+def sync_sets(*args, **kwargs):
+ flickr_api.set_keys(api_key=settings.FLICKR_API_KEY, api_secret=settings.FLICKR_API_SECRET)
+ flickr_api.set_auth_handler("app/photos/flickrauth")
+ user = flickr_api.test.login()
+ photosets = user.getPhotosets()
+ # reverse! reverse!
+ photosets.reverse()
+ disregard = [
+ 'POTD 2008',
+ 'Snow Day',
+ 'Wedding',
+ 'Some random stuff',
+ 'Lilah & Olivia',
+ '6 months+',
+ '6-9 months',
+ '9-18 months',
+ ]
+ for photoset in photosets:
+ if photoset['title'] in disregard:
+ pass
+ else:
+ try:
+ row = PhotoGallery.objects.get(set_id__exact=photoset['id'])
+ print('%s %s %s' % ('already have', row.set_title, 'moving on...'))
+ # okay it already exists, but is it up-to-date?
+ #get_photos_in_set(row,set.id)
+ except ObjectDoesNotExist:
+ s = PhotoGallery.objects.create(
+ set_id=force_unicode(photoset['id']),
+ set_title=force_unicode(photoset['title']),
+ set_desc=force_unicode(photoset['description']),
+ set_slug=slugify(force_unicode(photoset['title'])),
+ primary=force_unicode(photoset['primary']),
+ pub_date=datetime.datetime.fromtimestamp(float(photoset['date_create']))
+ )
+
+ get_photos_in_set(photoset, s)
+ #create the gallery thumbnail image:
+ photo = Photo.objects.get(flickr_id__exact=str(photoset['primary']))
+ make_gallery_thumb(photo, s)
+
+
+def get_photos_in_set(flickr_photoset, photoset):
+ for photo in flickr_photoset.getPhotos():
+ try:
+ p = Photo.objects.get(flickr_id__exact=str(photo['id']))
+ except ObjectDoesNotExist:
+ p = get_photo(photo)
+ if p.is_public:
+ photoset.photos.add(p)
+ slideshow_image(p, 1000, 800, 95)
+
+
+################################################
+## Various meta data and geo helper functions ##
+################################################
+
+
+def exif_handler(data):
+ converted = {}
+ try:
+ for t in data:
+ converted[t['tag']] = t['raw']
+ except:
+ pass
+ for k, v in EXIF_PARAMS.items():
+ if not converted.has_key(k):
+ converted[k] = v
+ return converted
+
+
+def flickr_datetime_to_datetime(fdt):
+ from datetime import datetime
+ from time import strptime
+ date_parts = strptime(fdt, '%Y-%m-%d %H:%M:%S')
+ return datetime(*date_parts[0:6])
+
+def get_geo(lat,lon):
+ from locations.models import Location, Region
+ from django.contrib.gis.geos import Point
+ pnt_wkt = Point(lon, lat)
+ try:
+ location = Location.objects.get(geometry__contains=pnt_wkt)
+ except Location.DoesNotExist:
+ location = None
+ try:
+ region = Region.objects.get(geometry__contains=pnt_wkt)
+ except Region.DoesNotExist:
+ region = None
+ return location, region
+
+#######################################################################
+## Photo retrieval functions to pull down images from Flickr servers ##
+#######################################################################
+
+def slideshow_image(photo,max_width, max_height, quality):
+ slide_dir = settings.IMAGES_ROOT + '/slideshow/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(slide_dir):
+ os.makedirs(slide_dir)
+
+ # Is it a retina image or not?
+ if max_width >= 1001 or max_height >= 801:
+ filename = '%s/%sx2.jpg' %(slide_dir, photo.flickr_id)
+ else:
+ filename = '%s/%s.jpg' %(slide_dir, photo.flickr_id)
+
+ flickr_photo = photo.get_original_url()
+ fname = urllib.urlopen(flickr_photo)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ cur_width, cur_height = img.size
+ #if image landscape
+ if cur_width > cur_height:
+ new_width = max_width
+ #check to make sure we aren't upsizing
+ if cur_width > new_width:
+ ratio = float(new_width)/cur_width
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ else:
+ #image portrait
+ new_height = max_height
+ #check to make sure we aren't upsizing
+ if cur_height > new_height:
+ ratio = float(new_height)/cur_height
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS)
+ resized.save(filename, 'JPEG', quality=quality, optimize=True)
+ else:
+ img.save(filename)
+ photo.slideshowimage_width = photo.get_width
+ photo.slideshowimage_height = photo.get_height
+ photo.slideshowimage_margintop = photo.get_margin_top
+ photo.slideshowimage_marginleft = photo.get_margin_left
+ photo.save()
+ #now resize the local copy
+
+
+
+def make_local_copies(photo):
+ orig_dir = settings.IMAGES_ROOT + '/flickr/full/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(orig_dir):
+ os.makedirs(orig_dir)
+ full = photo.get_original_url()
+ fname = urllib.urlopen(full)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_full = '%s/%s.jpg' %(orig_dir, photo.flickr_id)
+ img.save(local_full)
+ #save large size
+ large_dir = settings.IMAGES_ROOT + '/flickr/large/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(large_dir):
+ os.makedirs(large_dir)
+ large = photo.get_large_url()
+ fname = urllib.urlopen(large)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_large = '%s/%s.jpg' %(large_dir, photo.flickr_id)
+ if img.format == 'JPEG':
+ img.save(local_large)
+ #save medium size
+ med_dir = settings.IMAGES_ROOT + '/flickr/med/'+ photo.pub_date.strftime("%Y")
+ if not os.path.isdir(med_dir):
+ os.makedirs(med_dir)
+ med = photo.get_medium_url()
+ fname = urllib.urlopen(med)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+ local_med = '%s/%s.jpg' %(med_dir, photo.flickr_id)
+ img.save(local_med)
+
+def make_gallery_thumb(photo,set):
+ crop_dir = settings.IMAGES_ROOT + '/gallery_thumbs/'
+ if not os.path.isdir(crop_dir):
+ os.makedirs(crop_dir)
+ remote = photo.get_original_url()
+ print(remote)
+ fname = urllib.urlopen(remote)
+ im = cStringIO.StringIO(fname.read()) # constructs a StringIO holding the image
+ img = Image.open(im)
+
+ #calculate crop:
+ cur_width, cur_height = img.size
+ new_width, new_height = 291, 350
+ ratio = max(float(new_width)/cur_width,float(new_height)/cur_height)
+ x = (cur_width * ratio)
+ y = (cur_height * ratio)
+ xd = abs(new_width - x)
+ yd = abs(new_height - y)
+ x_diff = int(xd / 2)
+ y_diff = int(yd / 2)
+ box = (int(x_diff), int(y_diff), int(x_diff+new_width), int(y_diff+new_height))
+
+ #create resized file
+ resized = img.resize((int(x), int(y)), Image.ANTIALIAS).crop(box)
+ # save resized file
+ resized_filename = '%s/%s.jpg' %(crop_dir, set.id)
+ try:
+ if img.format == 'JPEG':
+ resized.save(resized_filename, 'JPEG', quality=95, optimize=True)
+ else:
+ resized.save(resized_filename)
+ except IOError, e:
+ if os.path.isfile(resized_filename):
+ os.unlink(resized_filename)
+ raise e
+ #os.unlink(img)
+
+
+
diff --git a/app/photos/static/image-preview.js b/app/photos/static/image-preview.js
new file mode 100644
index 0000000..b8fead5
--- /dev/null
+++ b/app/photos/static/image-preview.js
@@ -0,0 +1,42 @@
+function build_image_preview () {
+ var url = window.location.href
+ var cur = url.split('/')[6];
+ if (cur) {
+ var container = document.createElement("div");
+ container.className = "form-row field-image";
+ var wrapper = document.createElement("div");
+ var label = document.createElement("label");
+ label.textContent = "Image:";
+ var pwrap = document.createElement("p");
+ var img = document.createElement("img");
+
+ var request = new XMLHttpRequest();
+ request.open('GET', '/photos/luximage/data/admin/preview/'+cur+'/', true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ //console.log(data);
+ img.src = data['url'];
+ } else {
+ console.log("server error");
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ pwrap.appendChild(img);
+ wrapper.appendChild(label);
+ wrapper.appendChild(pwrap);
+ container.appendChild(wrapper);
+ parent = document.getElementById("luximage_form");
+ node = parent.children[1].children[0];
+ node.parentNode.insertBefore(container, node.previousSibling);
+ } else {
+ return;
+ }
+}
+
+document.addEventListener("DOMContentLoaded", function(event) {
+ build_image_preview();
+});
diff --git a/app/photos/static/my_styles.css b/app/photos/static/my_styles.css
new file mode 100644
index 0000000..d13c8e4
--- /dev/null
+++ b/app/photos/static/my_styles.css
@@ -0,0 +1,40 @@
+
+/*o.v.*/
+
+#id_featured_image {
+ /*style the "box" in its minimzed state*/
+ border:1px solid black; width:230px; overflow:hidden;
+ height:300px; overflow-y:scroll;
+ /*animate collapsing the dropdown from open to closed state (v. fast)*/
+}
+#id_featured_image input {
+ /*hide the nasty default radio buttons. like, completely!*/
+ position:absolute;top:0;left:0;opacity:0;
+}
+
+
+#id_featured_image label {
+ /*style the labels to look like dropdown options, kinda*/
+ color: #000;
+ display:block;
+ margin: 2px 2px 2px 10px;
+ height:102px;
+ opacity:.6;
+ background-repeat: no-repeat;
+}
+#id_featured_image:hover label{
+ /*this is how labels render in the "expanded" state. we want to see only the selected radio button in the collapsed menu, and all of them when expanded*/
+}
+#id_featured_image label:hover {
+ opacity:.8;
+}
+#id_featured_image input:checked + label {
+ /*tricky! labels immediately following a checked radio button (with our markup they are semantically related) should be fully opaque regardless of hover, and they should always be visible (i.e. even in the collapsed menu*/
+ opacity:1 !important;
+ display:block;
+ background: #333;
+}
+
+/*pfft, nothing as cool here, just the value trace*/
+#trace {margin:0 0 20px;}
+#id_featured_image li:first-child { display: none;}
diff --git a/app/photos/templatetags/__init__.py b/app/photos/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/photos/templatetags/__init__.py
diff --git a/app/photos/templatetags/get_image_by_size.py b/app/photos/templatetags/get_image_by_size.py
new file mode 100644
index 0000000..c56c44e
--- /dev/null
+++ b/app/photos/templatetags/get_image_by_size.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def get_image_by_size(obj, *args):
+ method = getattr(obj, "get_image_by_size")
+ return method(*args)
diff --git a/app/photos/templatetags/get_image_width.py b/app/photos/templatetags/get_image_width.py
new file mode 100644
index 0000000..ac39184
--- /dev/null
+++ b/app/photos/templatetags/get_image_width.py
@@ -0,0 +1,9 @@
+from math import floor
+from django import template
+
+register = template.Library()
+
+@register.simple_tag
+def get_image_width(obj, size, *args):
+ ratio = floor(int(size)*100/int(obj.height))/100
+ return floor(ratio*int(obj.height))
diff --git a/app/photos/urls.py b/app/photos/urls.py
new file mode 100644
index 0000000..73d906c
--- /dev/null
+++ b/app/photos/urls.py
@@ -0,0 +1,56 @@
+from django.urls import path, re_path
+from django.views.generic.base import RedirectView
+
+from . import views
+
+app_name = "photos"
+
+urlpatterns = [
+ path(
+ r'daily/<int:page>',
+ views.DailyPhotoList.as_view(),
+ name="daily_photo_list"
+ ),
+ path(
+ r'daily/',
+ views.DailyPhotoList.as_view(),
+ {'page': 1},
+ name="daily_photo_list"
+ ),
+ path(
+ r'data/(<str:slug>/$',
+ views.photo_json
+ ),
+ path(
+ r'data/admin/preview/<int:pk>/',
+ views.photo_preview_json,
+ name="admin_image_preview"
+ ),
+ path(
+ r'data/admin/tn/<int:pk>/',
+ views.thumb_preview_json,
+ name="admin_thumb_preview"
+ ),
+ re_path(
+ r'galleries/(?P<slug>[-\w]+)$',
+ views.Gallery.as_view(),
+ name="private"
+ ),
+ re_path(
+ r'galleries/(?P<page>\d+)/$',
+ views.GalleryList.as_view(),
+ name="private_list"
+ ),
+ path(
+ r'galleries/',
+ RedirectView.as_view(url="/photos/galleries/1/", permanent=False)
+ ),
+ path(
+ r'<str:slug>/',
+ RedirectView.as_view(url="/photos/%(slug)s/1/", permanent=False)
+ ),
+ re_path(
+ r'',
+ RedirectView.as_view(url="/photos/1/", permanent=False)
+ ),
+]
diff --git a/app/photos/utils.py b/app/photos/utils.py
new file mode 100644
index 0000000..84e72f5
--- /dev/null
+++ b/app/photos/utils.py
@@ -0,0 +1,28 @@
+import os
+import re
+import subprocess
+
+from django.apps import apps
+from django.conf import settings
+
+from PIL import ImageFile
+from bs4 import BeautifulSoup
+# pip install python-resize-image
+from resizeimage import resizeimage
+
+
+def resize_image(img, width=None, height=None, quality=72, base_path="", filename=""):
+ if width and height:
+ newimg = resizeimage.resize_cover(img, [width, height])
+ if width and not height:
+ newimg = resizeimage.resize_width(img, width)
+ if height and not width:
+ newimg = resizeimage.resize_height(img, height)
+ if not os.path.isdir(base_path):
+ os.makedirs(base_path)
+ path = "%s%s" % (base_path, filename)
+ ImageFile.MAXBLOCK = img.size[0] * img.size[1] * 4
+ newimg.save(path, newimg.format, quality=quality)
+ subprocess.call(["jpegoptim", "%s" % path])
+
+
diff --git a/app/photos/views.py b/app/photos/views.py
new file mode 100644
index 0000000..4581e07
--- /dev/null
+++ b/app/photos/views.py
@@ -0,0 +1,59 @@
+import json
+from django.shortcuts import render_to_response, render
+from django.template import RequestContext
+from django.http import Http404, HttpResponse
+from django.core import serializers
+
+from .models import Photo, PhotoGallery, LuxGallery, LuxImage
+from locations.models import Country, Region
+
+from utils.views import PaginatedListView
+from django.views.generic import ListView
+from django.views.generic.detail import DetailView
+
+
+class Gallery(DetailView):
+ model = LuxGallery
+ slug_field = "slug"
+ template_name = "details/photo_gallery.html"
+
+
+class GalleryList(PaginatedListView):
+ template_name = 'archives/gallery_list.html'
+
+ def get_queryset(self):
+ return LuxGallery.objects.filter(is_public=True)
+
+ def get_context_data(self, **kwargs):
+ # Call the base implementation first to get a context
+ context = super(GalleryList, self).get_context_data(**kwargs)
+ context['is_private'] = False
+ return context
+
+
+class DailyPhotoList(PaginatedListView):
+ template_name = 'archives/photo_daily_list.html'
+
+ def get_queryset(self):
+ return LuxImage.objects.filter(is_public=True, title__startswith="daily_")
+
+
+def photo_json(request, slug):
+ p = PhotoGallery.objects.filter(set_slug=slug)
+ return HttpResponse(serializers.serialize('json', p), mimetype='application/json')
+
+
+def photo_preview_json(request, pk):
+ p = LuxImage.objects.get(pk=pk)
+ data = {}
+ data['url'] = p.get_admin_image()
+ data = json.dumps(data)
+ return HttpResponse(data)
+
+
+def thumb_preview_json(request, pk):
+ p = LuxImage.objects.get(pk=pk)
+ data = {}
+ data['url'] = p.get_admin_insert()
+ data = json.dumps(data)
+ return HttpResponse(data)
diff --git a/app/taxonomy/migrations/0001_initial.py b/app/taxonomy/migrations/0001_initial.py
new file mode 100644
index 0000000..a181161
--- /dev/null
+++ b/app/taxonomy/migrations/0001_initial.py
@@ -0,0 +1,52 @@
+# Generated by Django 2.1.7 on 2019-03-30 17:07
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Category',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=250)),
+ ('color_rgb', models.CharField(blank=True, max_length=20)),
+ ('slug', models.SlugField(blank=True)),
+ ('date_created', models.DateTimeField(auto_now_add=True)),
+ ('date_updated', models.DateTimeField(auto_now=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='LuxTag',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100, unique=True, verbose_name='Name')),
+ ('slug', models.SlugField(max_length=100, unique=True, verbose_name='Slug')),
+ ('color_rgb', models.CharField(blank=True, max_length=20)),
+ ],
+ options={
+ 'verbose_name_plural': 'Tags',
+ 'verbose_name': 'Tag',
+ },
+ ),
+ migrations.CreateModel(
+ name='TaggedItems',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('object_id', models.IntegerField(db_index=True, verbose_name='Object id')),
+ ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_tagged_items', to='contenttypes.ContentType', verbose_name='Content type')),
+ ('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taxonomy_taggeditems_items', to='taxonomy.LuxTag')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/app/taxonomy/migrations/__init__.py b/app/taxonomy/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/taxonomy/migrations/__init__.py
diff --git a/app/taxonomy/models.py b/app/taxonomy/models.py
new file mode 100644
index 0000000..df38ae7
--- /dev/null
+++ b/app/taxonomy/models.py
@@ -0,0 +1,39 @@
+from django.db import models
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.utils.functional import cached_property
+
+from taggit.models import TagBase, GenericTaggedItemBase
+
+
+class LuxTag(TagBase):
+ ''' override the default taggit model to add some color '''
+ color_rgb = models.CharField(max_length=20, blank=True)
+
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+ @cached_property
+ def get_absolute_url(self):
+ return reverse("taxonomy:tags", kwargs={"slug": self.slug})
+
+
+class TaggedItems(GenericTaggedItemBase):
+ ''' necessary with custom tag model, lets you still use TaggableManager'''
+ tag = models.ForeignKey(LuxTag, related_name="%(app_label)s_%(class)s_items", on_delete=models.CASCADE)
+
+
+class Category(models.Model):
+ """ Generic model for Categories """
+ name = models.CharField(max_length=250)
+ color_rgb = models.CharField(max_length=20, blank=True)
+ slug = models.SlugField(blank=True)
+ date_created = models.DateTimeField(blank=True, auto_now_add=True, editable=False)
+ date_updated = models.DateTimeField(blank=True, auto_now=True, editable=False)
+
+ def __str__(self):
+ return self.name
+
+ def get_absolute_url(self):
+ return reverse("taxonomy:categories", kwargs={"slug": self.slug})
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/utils/__init__.py
diff --git a/app/utils/forms.py b/app/utils/forms.py
new file mode 100644
index 0000000..c1216a1
--- /dev/null
+++ b/app/utils/forms.py
@@ -0,0 +1,7 @@
+from django import forms
+
+
+class BaseFormWithUser(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ self.user = kwargs.pop("user", None)
+ super(BaseFormWithUser, self).__init__(*args, **kwargs)
diff --git a/app/utils/next_prev.py b/app/utils/next_prev.py
new file mode 100644
index 0000000..766add1
--- /dev/null
+++ b/app/utils/next_prev.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# from https://github.com/gregplaysguitar/django-next-prev/blob/master/next_prev.py
+
+from functools import partial
+
+from django.db import models
+
+if not locals().get('reduce'):
+ from functools import reduce
+
+__version__ = '1.0.1'
+VERSION = tuple(map(int, __version__.split('.')))
+
+
+def get_model_attr(instance, attr):
+ """Example usage: get_model_attr(instance, 'category__slug')"""
+ for field in attr.split('__'):
+ instance = getattr(instance, field)
+ return instance
+
+
+def next_or_prev_in_order(instance, qs=None, prev=False, loop=False):
+ """Get the next (or previous with prev=True) item for instance, from the
+ given queryset (which is assumed to contain instance) respecting
+ queryset ordering. If loop is True, return the first/last item when the
+ end/start is reached. """
+
+ if not qs:
+ qs = instance.__class__.objects.all()
+
+ if prev:
+ qs = qs.reverse()
+ lookup = 'lt'
+ else:
+ lookup = 'gt'
+
+ q_list = []
+ prev_fields = []
+
+ if qs.query.extra_order_by:
+ ordering = qs.query.extra_order_by
+ elif qs.query.order_by:
+ ordering = qs.query.order_by
+ elif qs.query.get_meta().ordering:
+ ordering = qs.query.get_meta().ordering
+ else:
+ ordering = []
+
+ ordering = list(ordering)
+
+ # if the ordering doesn't contain pk, append it and reorder the queryset
+ # to ensure consistency
+ if 'pk' not in ordering and '-pk' not in ordering:
+ ordering.append('pk')
+ qs = qs.order_by(*ordering)
+
+ for field in ordering:
+ if field[0] == '-':
+ this_lookup = (lookup == 'gt' and 'lt' or 'gt')
+ field = field[1:]
+ else:
+ this_lookup = lookup
+ q_kwargs = dict([(f, get_model_attr(instance, f))
+ for f in prev_fields])
+ key = "%s__%s" % (field, this_lookup)
+ q_kwargs[key] = get_model_attr(instance, field)
+ q_list.append(models.Q(**q_kwargs))
+ prev_fields.append(field)
+ try:
+ return qs.filter(reduce(models.Q.__or__, q_list))[0]
+ except IndexError:
+ length = qs.count()
+ if loop and length > 1:
+ # queryset is reversed above if prev
+ return qs[0]
+ return None
+
+
+next_in_order = partial(next_or_prev_in_order, prev=False)
+prev_in_order = partial(next_or_prev_in_order, prev=True)
diff --git a/app/utils/static/autocomplete.js b/app/utils/static/autocomplete.js
new file mode 100644
index 0000000..ad0c70d
--- /dev/null
+++ b/app/utils/static/autocomplete.js
@@ -0,0 +1,10 @@
+function autoCompleteItems() {
+var item = document.getElementById('id_ap');
+var singlePresetOpts = new Choices(item, {
+ searchPlaceholderValue: 'Search for Animal',
+ placeholder: true,
+});
+}
+document.addEventListener("DOMContentLoaded", function(event) {
+ autoCompleteItems();
+});
diff --git a/app/utils/static/choices.css b/app/utils/static/choices.css
new file mode 100644
index 0000000..2c1e7f1
--- /dev/null
+++ b/app/utils/static/choices.css
@@ -0,0 +1,2 @@
+.choices{position:relative;margin-bottom:24px;font-size:16px; min-width:400px;}
+.choices:focus{outline:none}.choices:last-child{margin-bottom:0}.choices.is-disabled .choices__inner,.choices.is-disabled .choices__input{background-color:#eaeaea;cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.choices.is-disabled .choices__item{cursor:not-allowed}.choices[data-type*=select-one]{cursor:pointer}.choices[data-type*=select-one] .choices__inner{padding-bottom:7.5px}.choices[data-type*=select-one] .choices__input{display:block;width:100%;padding:10px;border-bottom:1px solid #ddd;background-color:#fff;margin:0}.choices[data-type*=select-one] .choices__button{background-image:url(../../icons/cross-inverse.svg);padding:0;background-size:8px;position:absolute;top:50%;right:0;margin-top:-10px;margin-right:25px;height:20px;width:20px;border-radius:10em;opacity:.5}.choices[data-type*=select-one] .choices__button:focus,.choices[data-type*=select-one] .choices__button:hover{opacity:1}.choices[data-type*=select-one] .choices__button:focus{box-shadow:0 0 0 2px #00bcd4}.choices[data-type*=select-one]:after{content:"";height:0;width:0;border-style:solid;border-color:#333 transparent transparent transparent;border-width:5px;position:absolute;right:11.5px;top:50%;margin-top:-2.5px;pointer-events:none}.choices[data-type*=select-one].is-open:after{border-color:transparent transparent #333 transparent;margin-top:-7.5px}.choices[data-type*=select-one][dir=rtl]:after{left:11.5px;right:auto}.choices[data-type*=select-one][dir=rtl] .choices__button{right:auto;left:0;margin-left:25px;margin-right:0}.choices[data-type*=select-multiple] .choices__inner,.choices[data-type*=text] .choices__inner{cursor:text}.choices[data-type*=select-multiple] .choices__button,.choices[data-type*=text] .choices__button{position:relative;display:inline-block;margin:0 -4px 0 8px;padding-left:16px;border-left:1px solid #008fa1;background-image:url(../../icons/cross.svg);background-size:8px;width:8px;line-height:1;opacity:.75}.choices[data-type*=select-multiple] .choices__button:focus,.choices[data-type*=select-multiple] .choices__button:hover,.choices[data-type*=text] .choices__button:focus,.choices[data-type*=text] .choices__button:hover{opacity:1}.choices__inner{display:inline-block;vertical-align:top;width:100%;background-color:#f9f9f9;padding:7.5px 7.5px 3.75px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;min-height:44px;overflow:hidden}.is-focused .choices__inner,.is-open .choices__inner{border-color:#b7b7b7}.is-open .choices__inner{border-radius:2.5px 2.5px 0 0}.is-flipped.is-open .choices__inner{border-radius:0 0 2.5px 2.5px}.choices__list{margin:0;padding-left:0;list-style:none}.choices__list--single{display:inline-block;padding:4px 16px 4px 4px;width:100%}[dir=rtl] .choices__list--single{padding-right:4px;padding-left:16px}.choices__list--single .choices__item{width:100%}.choices__list--multiple{display:inline}.choices__list--multiple .choices__item{display:inline-block;vertical-align:middle;border-radius:20px;padding:4px 10px;font-size:12px;font-weight:500;margin-right:3.75px;margin-bottom:3.75px;background-color:#00bcd4;border:1px solid #00a5bb;color:#fff;word-break:break-all}.choices__list--multiple .choices__item[data-deletable]{padding-right:5px}[dir=rtl] .choices__list--multiple .choices__item{margin-right:0;margin-left:3.75px}.choices__list--multiple .choices__item.is-highlighted{background-color:#00a5bb;border:1px solid #008fa1}.is-disabled .choices__list--multiple .choices__item{background-color:#aaa;border:1px solid #919191}.choices__list--dropdown{display:none;z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;border-bottom-left-radius:2.5px;border-bottom-right-radius:2.5px;overflow:hidden;word-break:break-all}.choices__list--dropdown.is-active{display:block}.is-open .choices__list--dropdown{border-color:#b7b7b7}.is-flipped .choices__list--dropdown{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-radius:.25rem .25rem 0 0}.choices__list--dropdown .choices__list{position:relative;max-height:300px;overflow:auto;-webkit-overflow-scrolling:touch;will-change:scroll-position}.choices__list--dropdown .choices__item{position:relative;padding:10px;font-size:14px}[dir=rtl] .choices__list--dropdown .choices__item{text-align:right}@media (min-width:640px){.choices__list--dropdown .choices__item--selectable{padding-right:100px}.choices__list--dropdown .choices__item--selectable:after{content:attr(data-select-text);font-size:12px;opacity:0;position:absolute;right:10px;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}[dir=rtl] .choices__list--dropdown .choices__item--selectable{text-align:right;padding-left:100px;padding-right:10px}[dir=rtl] .choices__list--dropdown .choices__item--selectable:after{right:auto;left:10px}}.choices__list--dropdown .choices__item--selectable.is-highlighted{background-color:#f2f2f2}.choices__list--dropdown .choices__item--selectable.is-highlighted:after{opacity:.5}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;opacity:.5}.choices__heading{font-weight:600;font-size:12px;padding:10px;border-bottom:1px solid #f7f7f7;color:gray}.choices__button{text-indent:-9999px;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.choices__button:focus{outline:none}.choices__input{display:inline-block;vertical-align:baseline;background-color:#f9f9f9;font-size:14px;margin-bottom:5px;border:0;border-radius:0;max-width:100%;padding:4px 0 4px 2px}.choices__input:focus{outline:0}[dir=rtl] .choices__input{padding-right:2px;padding-left:0}.choices__placeholder{opacity:.5}
diff --git a/app/utils/static/choices.min.js b/app/utils/static/choices.min.js
new file mode 100644
index 0000000..197f18e
--- /dev/null
+++ b/app/utils/static/choices.min.js
@@ -0,0 +1,5 @@
+/*! choices.js v3.0.3 | (c) 2018 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Choices=t():e.Choices=t()}(this,function(){return function(e){function t(n){if(i[n])return i[n].exports;var s=i[n]={exports:{},id:n,loaded:!1};return e[n].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var i={};return t.m=e,t.c=i,t.p="/assets/scripts/dist/",t(0)}([function(e,t,i){e.exports=i(1)},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t,i){return t in e?Object.defineProperty(e,t,{value:i,enumerable:!0,configurable:!0,writable:!0}):e[t]=i,e}function o(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var a=function(){function e(e,t){for(var i=0;i<t.length;i++){var n=t[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,i,n){return i&&e(t.prototype,i),n&&e(t,n),t}}(),c=i(2),l=n(c),h=i(3),u=n(h),d=i(4),f=n(d),p=i(31),v=i(32);i(33);var m=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"[data-choice]",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(r(this,e),(0,v.isType)("String",t)){var n=document.querySelectorAll(t);if(n.length>1)for(var s=1;s<n.length;s++){var o=n[s];new e(o,i)}}var a={silent:!1,items:[],choices:[],renderChoiceLimit:-1,maxItemCount:-1,addItems:!0,removeItems:!0,removeItemButton:!1,editItems:!1,duplicateItems:!0,delimiter:",",paste:!0,searchEnabled:!0,searchChoices:!0,searchFloor:1,searchResultLimit:4,searchFields:["label","value"],position:"auto",resetScrollPosition:!0,regexFilter:null,shouldSort:!0,shouldSortItems:!1,sortFilter:v.sortByAlpha,placeholder:!0,placeholderValue:null,searchPlaceholderValue:null,prependValue:null,appendValue:null,renderSelectedChoices:"auto",loadingText:"Loading...",noResultsText:"No results found",noChoicesText:"No choices to choose from",itemSelectText:"Press to select",addItemText:function(e){return'Press Enter to add <b>"'+e+'"</b>'},maxItemText:function(e){return"Only "+e+" values can be added."},uniqueItemText:"Only unique values can be added.",classNames:{containerOuter:"choices",containerInner:"choices__inner",input:"choices__input",inputCloned:"choices__input--cloned",list:"choices__list",listItems:"choices__list--multiple",listSingle:"choices__list--single",listDropdown:"choices__list--dropdown",item:"choices__item",itemSelectable:"choices__item--selectable",itemDisabled:"choices__item--disabled",itemChoice:"choices__item--choice",placeholder:"choices__placeholder",group:"choices__group",groupHeading:"choices__heading",button:"choices__button",activeState:"is-active",focusState:"is-focused",openState:"is-open",disabledState:"is-disabled",highlightedState:"is-highlighted",hiddenState:"is-hidden",flippedState:"is-flipped",loadingState:"is-loading",noResults:"has-no-results",noChoices:"has-no-choices"},fuseOptions:{include:"score"},callbackOnInit:null,callbackOnCreateTemplates:null};if(this.idNames={itemChoice:"item-choice"},this.config=(0,v.extend)(a,i),"auto"!==this.config.renderSelectedChoices&&"always"!==this.config.renderSelectedChoices&&(this.config.silent||console.warn("renderSelectedChoices: Possible values are 'auto' and 'always'. Falling back to 'auto'."),this.config.renderSelectedChoices="auto"),this.store=new f.default(this.render),this.initialised=!1,this.currentState={},this.prevState={},this.currentValue="",this.element=t,this.passedElement=(0,v.isType)("String",t)?document.querySelector(t):t,!this.passedElement)return void(this.config.silent||console.error("Passed element not found"));this.isTextElement="text"===this.passedElement.type,this.isSelectOneElement="select-one"===this.passedElement.type,this.isSelectMultipleElement="select-multiple"===this.passedElement.type,this.isSelectElement=this.isSelectOneElement||this.isSelectMultipleElement,this.isValidElementType=this.isTextElement||this.isSelectElement,this.isIe11=!(!navigator.userAgent.match(/Trident/)||!navigator.userAgent.match(/rv[ :]11/)),this.isScrollingOnIe=!1,this.config.shouldSortItems===!0&&this.isSelectOneElement&&(this.config.silent||console.warn("shouldSortElements: Type of passed element is 'select-one', falling back to false.")),this.highlightPosition=0,this.canSearch=this.config.searchEnabled,this.placeholder=!1,this.isSelectOneElement||(this.placeholder=!!this.config.placeholder&&(this.config.placeholderValue||this.passedElement.getAttribute("placeholder"))),this.presetChoices=this.config.choices,this.presetItems=this.config.items,this.passedElement.value&&(this.presetItems=this.presetItems.concat(this.passedElement.value.split(this.config.delimiter))),this.baseId=(0,v.generateId)(this.passedElement,"choices-"),this.render=this.render.bind(this),this._onFocus=this._onFocus.bind(this),this._onBlur=this._onBlur.bind(this),this._onKeyUp=this._onKeyUp.bind(this),this._onKeyDown=this._onKeyDown.bind(this),this._onClick=this._onClick.bind(this),this._onTouchMove=this._onTouchMove.bind(this),this._onTouchEnd=this._onTouchEnd.bind(this),this._onMouseDown=this._onMouseDown.bind(this),this._onMouseOver=this._onMouseOver.bind(this),this._onPaste=this._onPaste.bind(this),this._onInput=this._onInput.bind(this),this.wasTap=!0;var c="classList"in document.documentElement;c||this.config.silent||console.error("Choices: Your browser doesn't support Choices");var l=(0,v.isElement)(this.passedElement)&&this.isValidElementType;if(l){if("active"===this.passedElement.getAttribute("data-choice"))return;this.init()}else this.config.silent||console.error("Incompatible input passed")}return a(e,[{key:"init",value:function(){if(this.initialised!==!0){var e=this.config.callbackOnInit;this.initialised=!0,this._createTemplates(),this._createInput(),this.store.subscribe(this.render),this.render(),this._addEventListeners(),e&&(0,v.isType)("Function",e)&&e.call(this)}}},{key:"destroy",value:function(){if(this.initialised!==!1){this._removeEventListeners(),this.passedElement.classList.remove(this.config.classNames.input,this.config.classNames.hiddenState),this.passedElement.removeAttribute("tabindex");var e=this.passedElement.getAttribute("data-choice-orig-style");Boolean(e)?(this.passedElement.removeAttribute("data-choice-orig-style"),this.passedElement.setAttribute("style",e)):this.passedElement.removeAttribute("style"),this.passedElement.removeAttribute("aria-hidden"),this.passedElement.removeAttribute("data-choice"),this.passedElement.value=this.passedElement.value,this.containerOuter.parentNode.insertBefore(this.passedElement,this.containerOuter),this.containerOuter.parentNode.removeChild(this.containerOuter),this.clearStore(),this.config.templates=null,this.initialised=!1}}},{key:"renderGroups",value:function(e,t,i){var n=this,s=i||document.createDocumentFragment(),o=this.config.sortFilter;return this.config.shouldSort&&e.sort(o),e.forEach(function(e){var i=t.filter(function(t){return n.isSelectOneElement?t.groupId===e.id:t.groupId===e.id&&!t.selected});if(i.length>=1){var o=n._getTemplate("choiceGroup",e);s.appendChild(o),n.renderChoices(i,s,!0)}}),s}},{key:"renderChoices",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],s=t||document.createDocumentFragment(),r=this.config,a=r.renderSelectedChoices,c=r.searchResultLimit,l=r.renderChoiceLimit,h=this.isSearching?v.sortByScore:this.config.sortFilter,u=function(e){var t="auto"!==a||(i.isSelectOneElement||!e.selected);if(t){var n=i._getTemplate("choice",e);s.appendChild(n)}},d=e;"auto"!==a||this.isSelectOneElement||(d=e.filter(function(e){return!e.selected}));var f=d.reduce(function(e,t){return t.placeholder?e.placeholderChoices.push(t):e.normalChoices.push(t),e},{placeholderChoices:[],normalChoices:[]}),p=f.placeholderChoices,m=f.normalChoices;(this.config.shouldSort||this.isSearching)&&m.sort(h);var g=d.length,y=[].concat(o(p),o(m));this.isSearching?g=c:l>0&&!n&&(g=l);for(var b=0;b<g;b++)y[b]&&u(y[b]);return s}},{key:"renderItems",value:function(e){var t=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=i||document.createDocumentFragment();if(this.config.shouldSortItems&&!this.isSelectOneElement&&e.sort(this.config.sortFilter),this.isTextElement){var s=this.store.getItemsReducedToValues(e),o=s.join(this.config.delimiter);this.passedElement.setAttribute("value",o),this.passedElement.value=o}else{var r=document.createDocumentFragment();e.forEach(function(e){var i=t._getTemplate("option",e);r.appendChild(i)}),this.passedElement.innerHTML="",this.passedElement.appendChild(r)}return e.forEach(function(e){var i=t._getTemplate("item",e);n.appendChild(i)}),n}},{key:"render",value:function(){if(!this.store.isLoading()&&(this.currentState=this.store.getState(),this.currentState!==this.prevState)){if((this.currentState.choices!==this.prevState.choices||this.currentState.groups!==this.prevState.groups||this.currentState.items!==this.prevState.items)&&this.isSelectElement){var e=this.store.getGroupsFilteredByActive(),t=this.store.getChoicesFilteredByActive(),i=document.createDocumentFragment();this.choiceList.innerHTML="",this.config.resetScrollPosition&&(this.choiceList.scrollTop=0),e.length>=1&&this.isSearching!==!0?i=this.renderGroups(e,t,i):t.length>=1&&(i=this.renderChoices(t,i));var n=this.store.getItemsFilteredByActive(),s=this._canAddItem(n,this.input.value);if(i.childNodes&&i.childNodes.length>0)s.response?(this.choiceList.appendChild(i),this._highlightChoice()):this.choiceList.appendChild(this._getTemplate("notice",s.notice));else{var o=void 0,r=void 0;this.isSearching?(r=(0,v.isType)("Function",this.config.noResultsText)?this.config.noResultsText():this.config.noResultsText,o=this._getTemplate("notice",r,"no-results")):(r=(0,v.isType)("Function",this.config.noChoicesText)?this.config.noChoicesText():this.config.noChoicesText,o=this._getTemplate("notice",r,"no-choices")),this.choiceList.appendChild(o)}}if(this.currentState.items!==this.prevState.items){var a=this.store.getItemsFilteredByActive();if(this.itemList.innerHTML="",a&&a){var c=this.renderItems(a);c.childNodes&&this.itemList.appendChild(c)}}this.prevState=this.currentState}}},{key:"highlightItem",value:function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(!e)return this;var i=e.id,n=e.groupId,s=n>=0?this.store.getGroupById(n):null;return this.store.dispatch((0,p.highlightItem)(i,!0)),t&&(s&&s.value?(0,v.triggerEvent)(this.passedElement,"highlightItem",{id:i,value:e.value,label:e.label,groupValue:s.value}):(0,v.triggerEvent)(this.passedElement,"highlightItem",{id:i,value:e.value,label:e.label})),this}},{key:"unhighlightItem",value:function(e){if(!e)return this;var t=e.id,i=e.groupId,n=i>=0?this.store.getGroupById(i):null;return this.store.dispatch((0,p.highlightItem)(t,!1)),n&&n.value?(0,v.triggerEvent)(this.passedElement,"unhighlightItem",{id:t,value:e.value,label:e.label,groupValue:n.value}):(0,v.triggerEvent)(this.passedElement,"unhighlightItem",{id:t,value:e.value,label:e.label}),this}},{key:"highlightAll",value:function(){var e=this,t=this.store.getItems();return t.forEach(function(t){e.highlightItem(t)}),this}},{key:"unhighlightAll",value:function(){var e=this,t=this.store.getItems();return t.forEach(function(t){e.unhighlightItem(t)}),this}},{key:"removeItemsByValue",value:function(e){var t=this;if(!e||!(0,v.isType)("String",e))return this;var i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.value===e&&t._removeItem(i)}),this}},{key:"removeActiveItems",value:function(e){var t=this,i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.active&&e!==i.id&&t._removeItem(i)}),this}},{key:"removeHighlightedItems",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],i=this.store.getItemsFilteredByActive();return i.forEach(function(i){i.highlighted&&i.active&&(e._removeItem(i),t&&e._triggerChange(i.value))}),this}},{key:"showDropdown",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=document.body,i=document.documentElement,n=Math.max(t.scrollHeight,t.offsetHeight,i.clientHeight,i.scrollHeight,i.offsetHeight);this.containerOuter.classList.add(this.config.classNames.openState),this.containerOuter.setAttribute("aria-expanded","true"),this.dropdown.classList.add(this.config.classNames.activeState),this.dropdown.setAttribute("aria-expanded","true");var s=this.dropdown.getBoundingClientRect(),o=Math.ceil(s.top+window.scrollY+this.dropdown.offsetHeight),r=!1;return"auto"===this.config.position?r=o>=n:"top"===this.config.position&&(r=!0),r&&this.containerOuter.classList.add(this.config.classNames.flippedState),e&&this.canSearch&&document.activeElement!==this.input&&this.input.focus(),(0,v.triggerEvent)(this.passedElement,"showDropdown",{}),this}},{key:"hideDropdown",value:function(){var e=arguments.length>0&&void 0!==arguments[0]&&arguments[0],t=this.containerOuter.classList.contains(this.config.classNames.flippedState);return this.containerOuter.classList.remove(this.config.classNames.openState),this.containerOuter.setAttribute("aria-expanded","false"),this.dropdown.classList.remove(this.config.classNames.activeState),this.dropdown.setAttribute("aria-expanded","false"),t&&this.containerOuter.classList.remove(this.config.classNames.flippedState),e&&this.canSearch&&document.activeElement===this.input&&this.input.blur(),(0,v.triggerEvent)(this.passedElement,"hideDropdown",{}),this}},{key:"toggleDropdown",value:function(){var e=this.dropdown.classList.contains(this.config.classNames.activeState);return e?this.hideDropdown():this.showDropdown(!0),this}},{key:"getValue",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],i=this.store.getItemsFilteredByActive(),n=[];return i.forEach(function(i){e.isTextElement?n.push(t?i.value:i):i.active&&n.push(t?i.value:i)}),this.isSelectOneElement?n[0]:n}},{key:"setValue",value:function(e){var t=this;if(this.initialised===!0){var i=[].concat(o(e)),n=function(e){var i=(0,v.getType)(e);if("Object"===i){if(!e.value)return;t.isTextElement?t._addItem(e.value,e.label,e.id,void 0,e.customProperties,e.placeholder):t._addChoice(e.value,e.label,!0,!1,-1,e.customProperties,e.placeholder)}else"String"===i&&(t.isTextElement?t._addItem(e):t._addChoice(e,e,!0,!1,-1,null))};i.length>1?i.forEach(function(e){n(e)}):n(i[0])}return this}},{key:"setValueByChoice",value:function(e){var t=this;if(!this.isTextElement){var i=this.store.getChoices(),n=(0,v.isType)("Array",e)?e:[e];n.forEach(function(e){var n=i.find(function(t){return t.value===e});n?n.selected?t.config.silent||console.warn("Attempting to select choice already selected"):t._addItem(n.value,n.label,n.id,n.groupId,n.customProperties,n.placeholder,n.keyCode):t.config.silent||console.warn("Attempting to select choice that does not exist")})}return this}},{key:"setChoices",value:function(e,t,i){var n=this,s=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(this.initialised===!0&&this.isSelectElement){if(!(0,v.isType)("Array",e)||!t)return this;s&&this._clearChoices(),this._setLoading(!0),e&&e.length&&(this.containerOuter.classList.remove(this.config.classNames.loadingState),e.forEach(function(e){e.choices?n._addGroup(e,e.id||null,t,i):n._addChoice(e[t],e[i],e.selected,e.disabled,void 0,e.customProperties,e.placeholder)})),this._setLoading(!1)}return this}},{key:"clearStore",value:function(){return this.store.dispatch((0,p.clearAll)()),this}},{key:"clearInput",value:function(){return this.input.value&&(this.input.value=""),this.isSelectOneElement||this._setInputWidth(),!this.isTextElement&&this.config.searchEnabled&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0))),this}},{key:"enable",value:function(){if(this.initialised){this.passedElement.disabled=!1;var e=this.containerOuter.classList.contains(this.config.classNames.disabledState);e&&(this._addEventListeners(),this.passedElement.removeAttribute("disabled"),this.input.removeAttribute("disabled"),this.containerOuter.classList.remove(this.config.classNames.disabledState),this.containerOuter.removeAttribute("aria-disabled"),this.isSelectOneElement&&this.containerOuter.setAttribute("tabindex","0"))}return this}},{key:"disable",value:function(){if(this.initialised){this.passedElement.disabled=!0;var e=!this.containerOuter.classList.contains(this.config.classNames.disabledState);e&&(this._removeEventListeners(),this.passedElement.setAttribute("disabled",""),this.input.setAttribute("disabled",""),this.containerOuter.classList.add(this.config.classNames.disabledState),this.containerOuter.setAttribute("aria-disabled","true"),this.isSelectOneElement&&this.containerOuter.setAttribute("tabindex","-1"))}return this}},{key:"ajax",value:function(e){var t=this;return this.initialised===!0&&this.isSelectElement&&(requestAnimationFrame(function(){t._handleLoadingState(!0)}),e(this._ajaxCallback())),this}},{key:"_triggerChange",value:function(e){e&&(0,v.triggerEvent)(this.passedElement,"change",{value:e})}},{key:"_handleButtonAction",value:function(e,t){if(e&&t&&this.config.removeItems&&this.config.removeItemButton){var i=t.parentNode.getAttribute("data-id"),n=e.find(function(e){return e.id===parseInt(i,10)});this._removeItem(n),this._triggerChange(n.value),this.isSelectOneElement&&this._selectPlaceholderChoice()}}},{key:"_selectPlaceholderChoice",value:function(){var e=this.store.getPlaceholderChoice();e&&(this._addItem(e.value,e.label,e.id,e.groupId,null,e.placeholder),this._triggerChange(e.value))}},{key:"_handleItemAction",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];if(e&&t&&this.config.removeItems&&!this.isSelectOneElement){var s=t.getAttribute("data-id");e.forEach(function(e){e.id!==parseInt(s,10)||e.highlighted?n||e.highlighted&&i.unhighlightItem(e):i.highlightItem(e)}),document.activeElement!==this.input&&this.input.focus()}}},{key:"_handleChoiceAction",value:function(e,t){if(e&&t){var i=t.getAttribute("data-id"),n=this.store.getChoiceById(i),s=e[0]&&e[0].keyCode?e[0].keyCode:null,o=this.dropdown.classList.contains(this.config.classNames.activeState);if(n.keyCode=s,(0,v.triggerEvent)(this.passedElement,"choice",{choice:n}),n&&!n.selected&&!n.disabled){var r=this._canAddItem(e,n.value);r.response&&(this._addItem(n.value,n.label,n.id,n.groupId,n.customProperties,n.placeholder,n.keyCode),this._triggerChange(n.value))}this.clearInput(),o&&this.isSelectOneElement&&(this.hideDropdown(),this.containerOuter.focus())}}},{key:"_handleBackspace",value:function(e){if(this.config.removeItems&&e){var t=e[e.length-1],i=e.some(function(e){return e.highlighted});this.config.editItems&&!i&&t?(this.input.value=t.value,this._setInputWidth(),this._removeItem(t),this._triggerChange(t.value)):(i||this.highlightItem(t,!1),this.removeHighlightedItems(!0))}}},{key:"_canAddItem",value:function(e,t){var i=!0,n=(0,v.isType)("Function",this.config.addItemText)?this.config.addItemText(t):this.config.addItemText;(this.isSelectMultipleElement||this.isTextElement)&&this.config.maxItemCount>0&&this.config.maxItemCount<=e.length&&(i=!1,n=(0,v.isType)("Function",this.config.maxItemText)?this.config.maxItemText(this.config.maxItemCount):this.config.maxItemText),this.isTextElement&&this.config.addItems&&i&&this.config.regexFilter&&(i=this._regexFilter(t));var s=!e.some(function(e){return(0,v.isType)("String",t)?e.value===t.trim():e.value===t});return s||this.config.duplicateItems||this.isSelectOneElement||!i||(i=!1,n=(0,v.isType)("Function",this.config.uniqueItemText)?this.config.uniqueItemText(t):this.config.uniqueItemText),{response:i,notice:n}}},{key:"_handleLoadingState",value:function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],t=this.itemList.querySelector("."+this.config.classNames.placeholder);e?(this.containerOuter.classList.add(this.config.classNames.loadingState),this.containerOuter.setAttribute("aria-busy","true"),this.isSelectOneElement?t?t.innerHTML=this.config.loadingText:(t=this._getTemplate("placeholder",this.config.loadingText),this.itemList.appendChild(t)):this.input.placeholder=this.config.loadingText):(this.containerOuter.classList.remove(this.config.classNames.loadingState),this.isSelectOneElement?t.innerHTML=this.placeholder||"":this.input.placeholder=this.placeholder||"")}},{key:"_ajaxCallback",value:function(){var e=this;return function(t,i,n){if(t&&i){var s=(0,v.isType)("Object",t)?[t]:t;s&&(0,v.isType)("Array",s)&&s.length?(e._handleLoadingState(!1),e._setLoading(!0),s.forEach(function(t){if(t.choices){var s=t.id||null;e._addGroup(t,s,i,n)}else e._addChoice(t[i],t[n],t.selected,t.disabled,void 0,t.customProperties,t.placeholder)}),e._setLoading(!1),e.isSelectOneElement&&e._selectPlaceholderChoice()):e._handleLoadingState(!1),e.containerOuter.removeAttribute("aria-busy")}}}},{key:"_searchChoices",value:function(e){var t=(0,v.isType)("String",e)?e.trim():e,i=(0,v.isType)("String",this.currentValue)?this.currentValue.trim():this.currentValue;if(t.length>=1&&t!==i+" "){var n=this.store.getSearchableChoices(),s=t,o=(0,v.isType)("Array",this.config.searchFields)?this.config.searchFields:[this.config.searchFields],r=Object.assign(this.config.fuseOptions,{keys:o}),a=new l.default(n,r),c=a.search(s);return this.currentValue=t,this.highlightPosition=0,this.isSearching=!0,this.store.dispatch((0,p.filterChoices)(c)),c.length}return 0}},{key:"_handleSearch",value:function(e){if(e){var t=this.store.getChoices(),i=t.some(function(e){return!e.active});if(this.input===document.activeElement)if(e&&e.length>=this.config.searchFloor){var n=0;this.config.searchChoices&&(n=this._searchChoices(e)),(0,v.triggerEvent)(this.passedElement,"search",{value:e,resultCount:n})}else i&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0)))}}},{key:"_addEventListeners",value:function(){document.addEventListener("keyup",this._onKeyUp),document.addEventListener("keydown",this._onKeyDown),document.addEventListener("click",this._onClick),document.addEventListener("touchmove",this._onTouchMove),document.addEventListener("touchend",this._onTouchEnd),document.addEventListener("mousedown",this._onMouseDown),document.addEventListener("mouseover",this._onMouseOver),this.isSelectOneElement&&(this.containerOuter.addEventListener("focus",this._onFocus),this.containerOuter.addEventListener("blur",this._onBlur)),this.input.addEventListener("input",this._onInput),this.input.addEventListener("paste",this._onPaste),this.input.addEventListener("focus",this._onFocus),this.input.addEventListener("blur",this._onBlur)}},{key:"_removeEventListeners",value:function(){document.removeEventListener("keyup",this._onKeyUp),document.removeEventListener("keydown",this._onKeyDown),document.removeEventListener("click",this._onClick),document.removeEventListener("touchmove",this._onTouchMove),document.removeEventListener("touchend",this._onTouchEnd),document.removeEventListener("mousedown",this._onMouseDown),document.removeEventListener("mouseover",this._onMouseOver),this.isSelectOneElement&&(this.containerOuter.removeEventListener("focus",this._onFocus),this.containerOuter.removeEventListener("blur",this._onBlur)),this.input.removeEventListener("input",this._onInput),this.input.removeEventListener("paste",this._onPaste),this.input.removeEventListener("focus",this._onFocus),this.input.removeEventListener("blur",this._onBlur)}},{key:"_setInputWidth",value:function(){this.placeholder?this.input.value&&this.input.value.length>=this.placeholder.length/1.25&&(this.input.style.width=(0,v.getWidthOfInput)(this.input)):this.input.style.width=(0,v.getWidthOfInput)(this.input)}},{key:"_onKeyDown",value:function(e){var t,i=this;if(e.target===this.input||this.containerOuter.contains(e.target)){var n=e.target,o=this.store.getItemsFilteredByActive(),r=this.input===document.activeElement,a=this.dropdown.classList.contains(this.config.classNames.activeState),c=this.itemList&&this.itemList.children,l=String.fromCharCode(e.keyCode),h=46,u=8,d=13,f=65,p=27,m=38,g=40,y=33,b=34,E=e.ctrlKey||e.metaKey;this.isTextElement||!/[a-zA-Z0-9-_ ]/.test(l)||a||this.showDropdown(!0),this.canSearch=this.config.searchEnabled;var _=function(){E&&c&&(i.canSearch=!1,i.config.removeItems&&!i.input.value&&i.input===document.activeElement&&i.highlightAll())},S=function(){if(i.isTextElement&&n.value){var t=i.input.value,s=i._canAddItem(o,t);s.response&&(a&&i.hideDropdown(),i._addItem(t),i._triggerChange(t),i.clearInput())}if(n.hasAttribute("data-button")&&(i._handleButtonAction(o,n),e.preventDefault()),a){e.preventDefault();var r=i.dropdown.querySelector("."+i.config.classNames.highlightedState);r&&(o[0]&&(o[0].keyCode=d),i._handleChoiceAction(o,r))}else i.isSelectOneElement&&(a||(i.showDropdown(!0),e.preventDefault()))},I=function(){a&&(i.toggleDropdown(),i.containerOuter.focus())},w=function(){if(a||i.isSelectOneElement){a||i.showDropdown(!0),i.canSearch=!1;var t=e.keyCode===g||e.keyCode===b?1:-1,n=e.metaKey||e.keyCode===b||e.keyCode===y,s=void 0;if(n)s=t>0?Array.from(i.dropdown.querySelectorAll("[data-choice-selectable]")).pop():i.dropdown.querySelector("[data-choice-selectable]");else{var o=i.dropdown.querySelector("."+i.config.classNames.highlightedState);s=o?(0,v.getAdjacentEl)(o,"[data-choice-selectable]",t):i.dropdown.querySelector("[data-choice-selectable]")}s&&((0,v.isScrolledIntoView)(s,i.choiceList,t)||i._scrollToChoice(s,t),i._highlightChoice(s)),e.preventDefault()}},T=function(){!r||e.target.value||i.isSelectOneElement||(i._handleBackspace(o),e.preventDefault())},C=(t={},s(t,f,_),s(t,d,S),s(t,p,I),s(t,m,w),s(t,y,w),s(t,g,w),s(t,b,w),s(t,u,T),s(t,h,T),t);C[e.keyCode]&&C[e.keyCode]()}}},{key:"_onKeyUp",value:function(e){if(e.target===this.input){var t=this.input.value,i=this.store.getItemsFilteredByActive(),n=this._canAddItem(i,t);if(this.isTextElement){var s=this.dropdown.classList.contains(this.config.classNames.activeState);if(t){if(n.notice){var o=this._getTemplate("notice",n.notice);this.dropdown.innerHTML=o.outerHTML}n.response===!0?s||this.showDropdown():!n.notice&&s&&this.hideDropdown()}else s&&this.hideDropdown()}else{var r=46,a=8;e.keyCode!==r&&e.keyCode!==a||e.target.value?this.canSearch&&n.response&&this._handleSearch(this.input.value):!this.isTextElement&&this.isSearching&&(this.isSearching=!1,this.store.dispatch((0,p.activateChoices)(!0)))}this.canSearch=this.config.searchEnabled}}},{key:"_onInput",value:function(){this.isSelectOneElement||this._setInputWidth()}},{key:"_onTouchMove",value:function(){this.wasTap===!0&&(this.wasTap=!1)}},{key:"_onTouchEnd",value:function(e){var t=e.target||e.touches[0].target,i=this.dropdown.classList.contains(this.config.classNames.activeState);this.wasTap===!0&&this.containerOuter.contains(t)&&(t!==this.containerOuter&&t!==this.containerInner||this.isSelectOneElement||(this.isTextElement?document.activeElement!==this.input&&this.input.focus():i||this.showDropdown(!0)),e.stopPropagation()),this.wasTap=!0}},{key:"_onMouseDown",value:function(e){var t=e.target;if(t===this.choiceList&&this.isIe11&&(this.isScrollingOnIe=!0),this.containerOuter.contains(t)&&t!==this.input){var i=void 0,n=this.store.getItemsFilteredByActive(),s=e.shiftKey;(i=(0,v.findAncestorByAttrName)(t,"data-button"))?this._handleButtonAction(n,i):(i=(0,v.findAncestorByAttrName)(t,"data-item"))?this._handleItemAction(n,i,s):(i=(0,v.findAncestorByAttrName)(t,"data-choice"))&&this._handleChoiceAction(n,i),e.preventDefault()}}},{key:"_onClick",value:function(e){var t=e.target,i=this.dropdown.classList.contains(this.config.classNames.activeState),n=this.store.getItemsFilteredByActive();if(this.containerOuter.contains(t))t.hasAttribute("data-button")&&this._handleButtonAction(n,t),i?this.isSelectOneElement&&t!==this.input&&!this.dropdown.contains(t)&&this.hideDropdown(!0):this.isTextElement?document.activeElement!==this.input&&this.input.focus():this.canSearch?this.showDropdown(!0):(this.showDropdown(),this.containerOuter.focus());else{var s=n.some(function(e){return e.highlighted});s&&this.unhighlightAll(),this.containerOuter.classList.remove(this.config.classNames.focusState),i&&this.hideDropdown()}}},{key:"_onMouseOver",value:function(e){(e.target===this.dropdown||this.dropdown.contains(e.target))&&e.target.hasAttribute("data-choice")&&this._highlightChoice(e.target)}},{key:"_onPaste",value:function(e){e.target!==this.input||this.config.paste||e.preventDefault()}},{key:"_onFocus",value:function(e){var t=this,i=e.target;if(this.containerOuter.contains(i)){var n=this.dropdown.classList.contains(this.config.classNames.activeState),s={text:function(){i===t.input&&t.containerOuter.classList.add(t.config.classNames.focusState)},"select-one":function(){t.containerOuter.classList.add(t.config.classNames.focusState),i===t.input&&(n||t.showDropdown())},"select-multiple":function(){i===t.input&&(t.containerOuter.classList.add(t.config.classNames.focusState),n||t.showDropdown(!0))}};s[this.passedElement.type]()}}},{key:"_onBlur",value:function(e){var t=this,i=e.target;if(this.containerOuter.contains(i)&&!this.isScrollingOnIe){var n=this.store.getItemsFilteredByActive(),s=this.dropdown.classList.contains(this.config.classNames.activeState),o=n.some(function(e){return e.highlighted}),r={text:function(){i===t.input&&(t.containerOuter.classList.remove(t.config.classNames.focusState),o&&t.unhighlightAll(),s&&t.hideDropdown())},"select-one":function(){t.containerOuter.classList.remove(t.config.classNames.focusState),i===t.containerOuter&&s&&!t.canSearch&&t.hideDropdown(),i===t.input&&s&&t.hideDropdown()},"select-multiple":function(){i===t.input&&(t.containerOuter.classList.remove(t.config.classNames.focusState),s&&t.hideDropdown(),o&&t.unhighlightAll())}};r[this.passedElement.type]()}else this.isScrollingOnIe=!1,this.input.focus()}},{key:"_regexFilter",value:function(e){if(!e)return!1;var t=this.config.regexFilter,i=new RegExp(t.source,"i");return i.test(e)}},{key:"_scrollToChoice",value:function(e,t){var i=this;if(e){var n=this.choiceList.offsetHeight,s=e.offsetHeight,o=e.offsetTop+s,r=this.choiceList.scrollTop+n,a=t>0?this.choiceList.scrollTop+o-r:e.offsetTop,c=function e(){var n=4,s=i.choiceList.scrollTop,o=!1,r=void 0,c=void 0;t>0?(r=(a-s)/n,c=r>1?r:1,i.choiceList.scrollTop=s+c,s<a&&(o=!0)):(r=(s-a)/n,c=r>1?r:1,i.choiceList.scrollTop=s-c,s>a&&(o=!0)),o&&requestAnimationFrame(function(i){e(i,a,t)})};requestAnimationFrame(function(e){c(e,a,t)})}}},{key:"_highlightChoice",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,i=Array.from(this.dropdown.querySelectorAll("[data-choice-selectable]")),n=t;if(i&&i.length){var s=Array.from(this.dropdown.querySelectorAll("."+this.config.classNames.highlightedState));s.forEach(function(t){t.classList.remove(e.config.classNames.highlightedState),t.setAttribute("aria-selected","false")}),n?this.highlightPosition=i.indexOf(n):(n=i.length>this.highlightPosition?i[this.highlightPosition]:i[i.length-1],n||(n=i[0])),n.classList.add(this.config.classNames.highlightedState),n.setAttribute("aria-selected","true"),this.containerOuter.setAttribute("aria-activedescendant",n.id)}}},{key:"_addItem",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:-1,n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:-1,s=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,o=arguments.length>5&&void 0!==arguments[5]&&arguments[5],r=arguments.length>6&&void 0!==arguments[6]?arguments[6]:null,a=(0,v.isType)("String",e)?e.trim():e,c=r,l=this.store.getItems(),h=t||a,u=parseInt(i,10)||-1,d=n>=0?this.store.getGroupById(n):null,f=l?l.length+1:1;return this.config.prependValue&&(a=this.config.prependValue+a.toString()),this.config.appendValue&&(a+=this.config.appendValue.toString()),this.store.dispatch((0,p.addItem)(a,h,f,u,n,s,o,c)),
+this.isSelectOneElement&&this.removeActiveItems(f),d&&d.value?(0,v.triggerEvent)(this.passedElement,"addItem",{id:f,value:a,label:h,groupValue:d.value,keyCode:c}):(0,v.triggerEvent)(this.passedElement,"addItem",{id:f,value:a,label:h,keyCode:c}),this}},{key:"_removeItem",value:function(e){if(!e||!(0,v.isType)("Object",e))return this;var t=e.id,i=e.value,n=e.label,s=e.choiceId,o=e.groupId,r=o>=0?this.store.getGroupById(o):null;return this.store.dispatch((0,p.removeItem)(t,s)),r&&r.value?(0,v.triggerEvent)(this.passedElement,"removeItem",{id:t,value:i,label:n,groupValue:r.value}):(0,v.triggerEvent)(this.passedElement,"removeItem",{id:t,value:i,label:n}),this}},{key:"_addChoice",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,i=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=arguments.length>3&&void 0!==arguments[3]&&arguments[3],s=arguments.length>4&&void 0!==arguments[4]?arguments[4]:-1,o=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null,r=arguments.length>6&&void 0!==arguments[6]&&arguments[6],a=arguments.length>7&&void 0!==arguments[7]?arguments[7]:null;if("undefined"!=typeof e&&null!==e){var c=this.store.getChoices(),l=t||e,h=c?c.length+1:1,u=this.baseId+"-"+this.idNames.itemChoice+"-"+h;this.store.dispatch((0,p.addChoice)(e,l,h,s,n,u,o,r,a)),i&&this._addItem(e,l,h,void 0,o,r,a)}}},{key:"_clearChoices",value:function(){this.store.dispatch((0,p.clearChoices)())}},{key:"_addGroup",value:function(e,t){var i=this,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"value",s=arguments.length>3&&void 0!==arguments[3]?arguments[3]:"label",o=(0,v.isType)("Object",e)?e.choices:Array.from(e.getElementsByTagName("OPTION")),r=t?t:Math.floor((new Date).valueOf()*Math.random()),a=!!e.disabled&&e.disabled;o?(this.store.dispatch((0,p.addGroup)(e.label,r,!0,a)),o.forEach(function(e){var t=e.disabled||e.parentNode&&e.parentNode.disabled;i._addChoice(e[n],(0,v.isType)("Object",e)?e[s]:e.innerHTML,e.selected,t,r,e.customProperties,e.placeholder)})):this.store.dispatch((0,p.addGroup)(e.label,e.id,!1,e.disabled))}},{key:"_getTemplate",value:function(e){if(!e)return null;for(var t=this.config.templates,i=arguments.length,n=Array(i>1?i-1:0),s=1;s<i;s++)n[s-1]=arguments[s];return t[e].apply(t,n)}},{key:"_createTemplates",value:function(){var e=this,t=this.config.classNames,i={containerOuter:function(i){return(0,v.strToEl)('\n <div\n class="'+t.containerOuter+'"\n '+(e.isSelectElement?e.config.searchEnabled?'role="combobox" aria-autocomplete="list"':'role="listbox"':"")+'\n data-type="'+e.passedElement.type+'"\n '+(e.isSelectOneElement?'tabindex="0"':"")+'\n aria-haspopup="true"\n aria-expanded="false"\n dir="'+i+'"\n >\n </div>\n ')},containerInner:function(){return(0,v.strToEl)('\n <div class="'+t.containerInner+'"></div>\n ')},itemList:function(){var i,n=(0,u.default)(t.list,(i={},s(i,t.listSingle,e.isSelectOneElement),s(i,t.listItems,!e.isSelectOneElement),i));return(0,v.strToEl)('\n <div class="'+n+'"></div>\n ')},placeholder:function(e){return(0,v.strToEl)('\n <div class="'+t.placeholder+'">\n '+e+"\n </div>\n ")},item:function(i){var n,o=(0,u.default)(t.item,(n={},s(n,t.highlightedState,i.highlighted),s(n,t.itemSelectable,!i.highlighted),s(n,t.placeholder,i.placeholder),n));if(e.config.removeItemButton){var r;return o=(0,u.default)(t.item,(r={},s(r,t.highlightedState,i.highlighted),s(r,t.itemSelectable,!i.disabled),s(r,t.placeholder,i.placeholder),r)),(0,v.strToEl)('\n <div\n class="'+o+'"\n data-item\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n data-deletable\n '+(i.active?'aria-selected="true"':"")+"\n "+(i.disabled?'aria-disabled="true"':"")+"\n >\n "+i.label+'<!--\n --><button\n type="button"\n class="'+t.button+'"\n data-button\n aria-label="Remove item: \''+i.value+"'\"\n >\n Remove item\n </button>\n </div>\n ")}return(0,v.strToEl)('\n <div\n class="'+o+'"\n data-item\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n '+(i.active?'aria-selected="true"':"")+"\n "+(i.disabled?'aria-disabled="true"':"")+"\n >\n "+i.label+"\n </div>\n ")},choiceList:function(){return(0,v.strToEl)('\n <div\n class="'+t.list+'"\n dir="ltr"\n role="listbox"\n '+(e.isSelectOneElement?"":'aria-multiselectable="true"')+"\n >\n </div>\n ")},choiceGroup:function(e){var i=(0,u.default)(t.group,s({},t.itemDisabled,e.disabled));return(0,v.strToEl)('\n <div\n class="'+i+'"\n data-group\n data-id="'+e.id+'"\n data-value="'+e.value+'"\n role="group"\n '+(e.disabled?'aria-disabled="true"':"")+'\n >\n <div class="'+t.groupHeading+'">'+e.value+"</div>\n </div>\n ")},choice:function(i){var n,o=(0,u.default)(t.item,t.itemChoice,(n={},s(n,t.itemDisabled,i.disabled),s(n,t.itemSelectable,!i.disabled),s(n,t.placeholder,i.placeholder),n));return(0,v.strToEl)('\n <div\n class="'+o+'"\n data-select-text="'+e.config.itemSelectText+'"\n data-choice\n data-id="'+i.id+'"\n data-value="'+i.value+'"\n '+(i.disabled?'data-choice-disabled aria-disabled="true"':"data-choice-selectable")+'\n id="'+i.elementId+'"\n '+(i.groupId>0?'role="treeitem"':'role="option"')+"\n >\n "+i.label+"\n </div>\n ")},input:function(){var e=(0,u.default)(t.input,t.inputCloned);return(0,v.strToEl)('\n <input\n type="text"\n class="'+e+'"\n autocomplete="off"\n autocapitalize="off"\n spellcheck="false"\n role="textbox"\n aria-autocomplete="list"\n >\n ')},dropdown:function(){var e=(0,u.default)(t.list,t.listDropdown);return(0,v.strToEl)('\n <div\n class="'+e+'"\n aria-expanded="false"\n >\n </div>\n ')},notice:function(e){var i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",o=(0,u.default)(t.item,t.itemChoice,(i={},s(i,t.noResults,"no-results"===n),s(i,t.noChoices,"no-choices"===n),i));return(0,v.strToEl)('\n <div class="'+o+'">\n '+e+"\n </div>\n ")},option:function(e){return(0,v.strToEl)('\n <option value="'+e.value+'" selected>'+e.label+"</option>\n ")}},n=this.config.callbackOnCreateTemplates,o={};n&&(0,v.isType)("Function",n)&&(o=n.call(this,v.strToEl)),this.config.templates=(0,v.extend)(i,o)}},{key:"_setLoading",value:function(e){this.store.dispatch((0,p.setIsLoading)(e))}},{key:"_createInput",value:function(){var e=this,t=this.passedElement.getAttribute("dir")||"ltr",i=this._getTemplate("containerOuter",t),n=this._getTemplate("containerInner"),s=this._getTemplate("itemList"),o=this._getTemplate("choiceList"),r=this._getTemplate("input"),a=this._getTemplate("dropdown");this.containerOuter=i,this.containerInner=n,this.input=r,this.choiceList=o,this.itemList=s,this.dropdown=a,this.passedElement.classList.add(this.config.classNames.input,this.config.classNames.hiddenState),this.passedElement.tabIndex="-1";var c=this.passedElement.getAttribute("style");if(Boolean(c)&&this.passedElement.setAttribute("data-choice-orig-style",c),this.passedElement.setAttribute("style","display:none;"),this.passedElement.setAttribute("aria-hidden","true"),this.passedElement.setAttribute("data-choice","active"),(0,v.wrap)(this.passedElement,n),(0,v.wrap)(n,i),this.isSelectOneElement?r.placeholder=this.config.searchPlaceholderValue||"":this.placeholder&&(r.placeholder=this.placeholder,r.style.width=(0,v.getWidthOfInput)(r)),this.config.addItems||this.disable(),i.appendChild(n),i.appendChild(a),n.appendChild(s),this.isTextElement||a.appendChild(o),this.isSelectMultipleElement||this.isTextElement?n.appendChild(r):this.canSearch&&a.insertBefore(r,a.firstChild),this.isSelectElement){var l=Array.from(this.passedElement.getElementsByTagName("OPTGROUP"));if(this.highlightPosition=0,this.isSearching=!1,this._setLoading(!0),l&&l.length)l.forEach(function(t){e._addGroup(t,t.id||null)});else{var h=Array.from(this.passedElement.options),u=this.config.sortFilter,d=this.presetChoices;h.forEach(function(e){d.push({value:e.value,label:e.innerHTML,selected:e.selected,disabled:e.disabled||e.parentNode.disabled,placeholder:e.hasAttribute("placeholder")})}),this.config.shouldSort&&d.sort(u);var f=d.some(function(e){return e.selected});d.forEach(function(t,i){if(e.isSelectOneElement){var n=f||!f&&i>0;e._addChoice(t.value,t.label,!n||t.selected,!!n&&t.disabled,void 0,t.customProperties,t.placeholder)}else e._addChoice(t.value,t.label,t.selected,t.disabled,void 0,t.customProperties,t.placeholder)})}this._setLoading(!1)}else this.isTextElement&&this.presetItems.forEach(function(t){var i=(0,v.getType)(t);if("Object"===i){if(!t.value)return;e._addItem(t.value,t.label,t.id,void 0,t.customProperties,t.placeholder)}else"String"===i&&e._addItem(t)})}}]),e}();e.exports=m},function(e,t,i){!function(t){"use strict";function i(){console.log.apply(console,arguments)}function n(e,t){var i;this.list=e,this.options=t=t||{};for(i in a)a.hasOwnProperty(i)&&("boolean"==typeof a[i]?this.options[i]=i in t?t[i]:a[i]:this.options[i]=t[i]||a[i])}function s(e,t,i){var n,r,a,c,l,h;if(t){if(a=t.indexOf("."),a!==-1?(n=t.slice(0,a),r=t.slice(a+1)):n=t,c=e[n],null!==c&&void 0!==c)if(r||"string"!=typeof c&&"number"!=typeof c)if(o(c))for(l=0,h=c.length;l<h;l++)s(c[l],r,i);else r&&s(c,r,i);else i.push(c)}else i.push(e);return i}function o(e){return"[object Array]"===Object.prototype.toString.call(e)}function r(e,t){t=t||{},this.options=t,this.options.location=t.location||r.defaultOptions.location,this.options.distance="distance"in t?t.distance:r.defaultOptions.distance,this.options.threshold="threshold"in t?t.threshold:r.defaultOptions.threshold,this.options.maxPatternLength=t.maxPatternLength||r.defaultOptions.maxPatternLength,this.pattern=t.caseSensitive?e:e.toLowerCase(),this.patternLen=e.length,this.patternLen<=this.options.maxPatternLength&&(this.matchmask=1<<this.patternLen-1,this.patternAlphabet=this._calculatePatternAlphabet())}var a={id:null,caseSensitive:!1,include:[],shouldSort:!0,searchFn:r,sortFn:function(e,t){return e.score-t.score},getFn:s,keys:[],verbose:!1,tokenize:!1,matchAllTokens:!1,tokenSeparator:/ +/g,minMatchCharLength:1,findAllMatches:!1};n.VERSION="2.7.3",n.prototype.set=function(e){return this.list=e,e},n.prototype.search=function(e){this.options.verbose&&i("\nSearch term:",e,"\n"),this.pattern=e,this.results=[],this.resultMap={},this._keyMap=null,this._prepareSearchers(),this._startSearch(),this._computeScore(),this._sort();var t=this._format();return t},n.prototype._prepareSearchers=function(){var e=this.options,t=this.pattern,i=e.searchFn,n=t.split(e.tokenSeparator),s=0,o=n.length;if(this.options.tokenize)for(this.tokenSearchers=[];s<o;s++)this.tokenSearchers.push(new i(n[s],e));this.fullSeacher=new i(t,e)},n.prototype._startSearch=function(){var e,t,i,n,s=this.options,o=s.getFn,r=this.list,a=r.length,c=this.options.keys,l=c.length,h=null;if("string"==typeof r[0])for(i=0;i<a;i++)this._analyze("",r[i],i,i);else for(this._keyMap={},i=0;i<a;i++)for(h=r[i],n=0;n<l;n++){if(e=c[n],"string"!=typeof e){if(t=1-e.weight||1,this._keyMap[e.name]={weight:t},e.weight<=0||e.weight>1)throw new Error("Key weight has to be > 0 and <= 1");e=e.name}else this._keyMap[e]={weight:1};this._analyze(e,o(h,e,[]),h,i)}},n.prototype._analyze=function(e,t,n,s){var r,a,c,l,h,u,d,f,p,v,m,g,y,b,E,_=this.options,S=!1;if(void 0!==t&&null!==t){a=[];var I=0;if("string"==typeof t){if(r=t.split(_.tokenSeparator),_.verbose&&i("---------\nKey:",e),this.options.tokenize){for(b=0;b<this.tokenSearchers.length;b++){for(f=this.tokenSearchers[b],_.verbose&&i("Pattern:",f.pattern),p=[],g=!1,E=0;E<r.length;E++){v=r[E],m=f.search(v);var w={};m.isMatch?(w[v]=m.score,S=!0,g=!0,a.push(m.score)):(w[v]=1,this.options.matchAllTokens||a.push(1)),p.push(w)}g&&I++,_.verbose&&i("Token scores:",p)}for(l=a[0],u=a.length,b=1;b<u;b++)l+=a[b];l/=u,_.verbose&&i("Token score average:",l)}d=this.fullSeacher.search(t),_.verbose&&i("Full text score:",d.score),h=d.score,void 0!==l&&(h=(h+l)/2),_.verbose&&i("Score average:",h),y=!this.options.tokenize||!this.options.matchAllTokens||I>=this.tokenSearchers.length,_.verbose&&i("Check Matches",y),(S||d.isMatch)&&y&&(c=this.resultMap[s],c?c.output.push({key:e,score:h,matchedIndices:d.matchedIndices}):(this.resultMap[s]={item:n,output:[{key:e,score:h,matchedIndices:d.matchedIndices}]},this.results.push(this.resultMap[s])))}else if(o(t))for(b=0;b<t.length;b++)this._analyze(e,t[b],n,s)}},n.prototype._computeScore=function(){var e,t,n,s,o,r,a,c,l,h=this._keyMap,u=this.results;for(this.options.verbose&&i("\n\nComputing score:\n"),e=0;e<u.length;e++){for(n=0,s=u[e].output,o=s.length,c=1,t=0;t<o;t++)r=s[t].score,a=h?h[s[t].key].weight:1,l=r*a,1!==a?c=Math.min(c,l):(n+=l,s[t].nScore=l);1===c?u[e].score=n/o:u[e].score=c,this.options.verbose&&i(u[e])}},n.prototype._sort=function(){var e=this.options;e.shouldSort&&(e.verbose&&i("\n\nSorting...."),this.results.sort(e.sortFn))},n.prototype._format=function(){var e,t,n,s,o=this.options,r=o.getFn,a=[],c=this.results,l=o.include;for(o.verbose&&i("\n\nOutput:\n\n",c),n=o.id?function(e){c[e].item=r(c[e].item,o.id,[])[0]}:function(){},s=function(e){var t,i,n,s,o,r=c[e];if(l.length>0){if(t={item:r.item},l.indexOf("matches")!==-1)for(n=r.output,t.matches=[],i=0;i<n.length;i++)s=n[i],o={indices:s.matchedIndices},s.key&&(o.key=s.key),t.matches.push(o);l.indexOf("score")!==-1&&(t.score=c[e].score)}else t=r.item;return t},e=0,t=c.length;e<t;e++)n(e),a.push(s(e));return a},r.defaultOptions={location:0,distance:100,threshold:.6,maxPatternLength:32},r.prototype._calculatePatternAlphabet=function(){var e={},t=0;for(t=0;t<this.patternLen;t++)e[this.pattern.charAt(t)]=0;for(t=0;t<this.patternLen;t++)e[this.pattern.charAt(t)]|=1<<this.pattern.length-t-1;return e},r.prototype._bitapScore=function(e,t){var i=e/this.patternLen,n=Math.abs(this.options.location-t);return this.options.distance?i+n/this.options.distance:n?1:i},r.prototype.search=function(e){var t,i,n,s,o,r,a,c,l,h,u,d,f,p,v,m,g,y,b,E,_,S,I,w=this.options;if(e=w.caseSensitive?e:e.toLowerCase(),this.pattern===e)return{isMatch:!0,score:0,matchedIndices:[[0,e.length-1]]};if(this.patternLen>w.maxPatternLength){if(y=e.match(new RegExp(this.pattern.replace(w.tokenSeparator,"|"))),b=!!y)for(_=[],t=0,S=y.length;t<S;t++)I=y[t],_.push([e.indexOf(I),I.length-1]);return{isMatch:b,score:b?.5:1,matchedIndices:_}}for(s=w.findAllMatches,o=w.location,n=e.length,r=w.threshold,a=e.indexOf(this.pattern,o),E=[],t=0;t<n;t++)E[t]=0;for(a!=-1&&(r=Math.min(this._bitapScore(0,a),r),a=e.lastIndexOf(this.pattern,o+this.patternLen),a!=-1&&(r=Math.min(this._bitapScore(0,a),r))),a=-1,m=1,g=[],h=this.patternLen+n,t=0;t<this.patternLen;t++){for(c=0,l=h;c<l;)this._bitapScore(t,o+l)<=r?c=l:h=l,l=Math.floor((h-c)/2+c);for(h=l,u=Math.max(1,o-l+1),d=s?n:Math.min(o+l,n)+this.patternLen,f=Array(d+2),f[d+1]=(1<<t)-1,i=d;i>=u;i--)if(v=this.patternAlphabet[e.charAt(i-1)],v&&(E[i-1]=1),f[i]=(f[i+1]<<1|1)&v,0!==t&&(f[i]|=(p[i+1]|p[i])<<1|1|p[i+1]),f[i]&this.matchmask&&(m=this._bitapScore(t,i-1),m<=r)){if(r=m,a=i-1,g.push(a),a<=o)break;u=Math.max(1,2*o-a)}if(this._bitapScore(t+1,o)>r)break;p=f}return _=this._getMatchedIndices(E),{isMatch:a>=0,score:0===m?.001:m,matchedIndices:_}},r.prototype._getMatchedIndices=function(e){for(var t,i=[],n=-1,s=-1,o=0,r=e.length;o<r;o++)t=e[o],t&&n===-1?n=o:t||n===-1||(s=o-1,s-n+1>=this.options.minMatchCharLength&&i.push([n,s]),n=-1);return e[o-1]&&o-1-n+1>=this.options.minMatchCharLength&&i.push([n,o-1]),i},e.exports=n}(this)},function(e,t,i){var n,s;!function(){"use strict";function i(){for(var e=[],t=0;t<arguments.length;t++){var n=arguments[t];if(n){var s=typeof n;if("string"===s||"number"===s)e.push(n);else if(Array.isArray(n))e.push(i.apply(null,n));else if("object"===s)for(var r in n)o.call(n,r)&&n[r]&&e.push(r)}}return e.join(" ")}var o={}.hasOwnProperty;"undefined"!=typeof e&&e.exports?e.exports=i:(n=[],s=function(){return i}.apply(t,n),!(void 0!==s&&(e.exports=s)))}()},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var i=0;i<t.length;i++){var n=t[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}return function(t,i,n){return i&&e(t.prototype,i),n&&e(t,n),t}}(),a=i(5),c=i(26),l=n(c),h=function(){function e(){o(this,e),this.store=(0,a.createStore)(l.default,window.devToolsExtension?window.devToolsExtension():void 0)}return r(e,[{key:"getState",value:function(){return this.store.getState()}},{key:"dispatch",value:function(e){this.store.dispatch(e)}},{key:"subscribe",value:function(e){this.store.subscribe(e)}},{key:"isLoading",value:function(){var e=this.store.getState();return e.general.loading}},{key:"getItems",value:function(){var e=this.store.getState();return e.items}},{key:"getItemsFilteredByActive",value:function(){var e=this.getItems(),t=e.filter(function(e){return e.active===!0},[]);return t}},{key:"getItemsReducedToValues",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.getItems(),t=e.reduce(function(e,t){return e.push(t.value),e},[]);return t}},{key:"getChoices",value:function(){var e=this.store.getState();return e.choices}},{key:"getChoicesFilteredByActive",value:function(){var e=this.getChoices(),t=e.filter(function(e){return e.active===!0});return t}},{key:"getChoicesFilteredBySelectable",value:function(){var e=this.getChoices(),t=e.filter(function(e){return e.disabled!==!0});return t}},{key:"getSearchableChoices",value:function(){var e=this.getChoicesFilteredBySelectable();return e.filter(function(e){return e.placeholder!==!0})}},{key:"getChoiceById",value:function(e){if(e){var t=this.getChoicesFilteredByActive(),i=t.find(function(t){return t.id===parseInt(e,10)});return i}return!1}},{key:"getGroups",value:function(){var e=this.store.getState();return e.groups}},{key:"getGroupsFilteredByActive",value:function(){var e=this.getGroups(),t=this.getChoices(),i=e.filter(function(e){var i=e.active===!0&&e.disabled===!1,n=t.some(function(e){return e.active===!0&&e.disabled===!1});return i&&n},[]);return i}},{key:"getGroupById",value:function(e){var t=this.getGroups(),i=t.find(function(t){return t.id===e});return i}},{key:"getPlaceholderChoice",value:function(){var e=this.getChoices(),t=[].concat(s(e)).reverse().find(function(e){return e.placeholder===!0});return t}}]),e}();t.default=h,e.exports=h},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0,t.compose=t.applyMiddleware=t.bindActionCreators=t.combineReducers=t.createStore=void 0;var s=i(6),o=n(s),r=i(21),a=n(r),c=i(23),l=n(c),h=i(24),u=n(h),d=i(25),f=n(d),p=i(22);n(p);t.createStore=o.default,t.combineReducers=a.default,t.bindActionCreators=l.default,t.applyMiddleware=u.default,t.compose=f.default},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t,i){function n(){g===m&&(g=m.slice())}function o(){return v}function a(e){if("function"!=typeof e)throw new Error("Expected listener to be a function.");var t=!0;return n(),g.push(e),function(){if(t){t=!1,n();var i=g.indexOf(e);g.splice(i,1)}}}function h(e){if(!(0,r.default)(e))throw new Error("Actions must be plain objects. Use custom middleware for async actions.");if("undefined"==typeof e.type)throw new Error('Actions may not have an undefined "type" property. Have you misspelled a constant?');if(y)throw new Error("Reducers may not dispatch actions.");try{y=!0,v=p(v,e)}finally{y=!1}for(var t=m=g,i=0;i<t.length;i++){var n=t[i];n()}return e}function u(e){if("function"!=typeof e)throw new Error("Expected the nextReducer to be a function.");p=e,h({type:l.INIT})}function d(){var e,t=a;return e={subscribe:function(e){function i(){e.next&&e.next(o())}if("object"!=typeof e)throw new TypeError("Expected the observer to be an object.");i();var n=t(i);return{unsubscribe:n}}},e[c.default]=function(){return this},e}var f;if("function"==typeof t&&"undefined"==typeof i&&(i=t,t=void 0),"undefined"!=typeof i){if("function"!=typeof i)throw new Error("Expected the enhancer to be a function.");return i(s)(e,t)}if("function"!=typeof e)throw new Error("Expected the reducer to be a function.");var p=e,v=t,m=[],g=m,y=!1;return h({type:l.INIT}),f={dispatch:h,subscribe:a,getState:o,replaceReducer:u},f[c.default]=d,f}t.__esModule=!0,t.ActionTypes=void 0,t.default=s;var o=i(7),r=n(o),a=i(17),c=n(a),l=t.ActionTypes={INIT:"@@redux/INIT"}},function(e,t,i){function n(e){if(!r(e)||s(e)!=a)return!1;var t=o(e);if(null===t)return!0;var i=u.call(t,"constructor")&&t.constructor;return"function"==typeof i&&i instanceof i&&h.call(i)==d}var s=i(8),o=i(14),r=i(16),a="[object Object]",c=Function.prototype,l=Object.prototype,h=c.toString,u=l.hasOwnProperty,d=h.call(Object);e.exports=n},function(e,t,i){function n(e){return null==e?void 0===e?c:a:l&&l in Object(e)?o(e):r(e)}var s=i(9),o=i(12),r=i(13),a="[object Null]",c="[object Undefined]",l=s?s.toStringTag:void 0;e.exports=n},function(e,t,i){var n=i(10),s=n.Symbol;e.exports=s},function(e,t,i){var n=i(11),s="object"==typeof self&&self&&self.Object===Object&&self,o=n||s||Function("return this")();e.exports=o},function(e,t){(function(t){var i="object"==typeof t&&t&&t.Object===Object&&t;e.exports=i}).call(t,function(){return this}())},function(e,t,i){function n(e){var t=r.call(e,c),i=e[c];try{e[c]=void 0;var n=!0}catch(e){}var s=a.call(e);return n&&(t?e[c]=i:delete e[c]),s}var s=i(9),o=Object.prototype,r=o.hasOwnProperty,a=o.toString,c=s?s.toStringTag:void 0;e.exports=n},function(e,t){function i(e){return s.call(e)}var n=Object.prototype,s=n.toString;e.exports=i},function(e,t,i){var n=i(15),s=n(Object.getPrototypeOf,Object);e.exports=s},function(e,t){function i(e,t){return function(i){return e(t(i))}}e.exports=i},function(e,t){function i(e){return null!=e&&"object"==typeof e}e.exports=i},function(e,t,i){e.exports=i(18)},function(e,t,i){(function(e,n){"use strict";function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o,r=i(20),a=s(r);o="undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof e?e:n;var c=(0,a.default)(o);t.default=c}).call(t,function(){return this}(),i(19)(e))},function(e,t){e.exports=function(e){return e.webpackPolyfill||(e.deprecate=function(){},e.paths=[],e.children=[],e.webpackPolyfill=1),e}},function(e,t){"use strict";function i(e){var t,i=e.Symbol;return"function"==typeof i?i.observable?t=i.observable:(t=i("observable"),i.observable=t):t="@@observable",t}Object.defineProperty(t,"__esModule",{value:!0}),t.default=i},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(e,t){var i=t&&t.type,n=i&&'"'+i.toString()+'"'||"an action";return"Given action "+n+', reducer "'+e+'" returned undefined. To ignore an action, you must explicitly return the previous state. If you want this reducer to hold no value, you can return null instead of undefined.'}function o(e){Object.keys(e).forEach(function(t){var i=e[t],n=i(void 0,{type:a.ActionTypes.INIT});if("undefined"==typeof n)throw new Error('Reducer "'+t+"\" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.");var s="@@redux/PROBE_UNKNOWN_ACTION_"+Math.random().toString(36).substring(7).split("").join(".");if("undefined"==typeof i(void 0,{type:s}))throw new Error('Reducer "'+t+'" returned undefined when probed with a random type. '+("Don't try to handle "+a.ActionTypes.INIT+' or other actions in "redux/*" ')+"namespace. They are considered private. Instead, you must return the current state for any unknown actions, unless it is undefined, in which case you must return the initial state, regardless of the action type. The initial state may not be undefined, but can be null.")})}function r(e){for(var t=Object.keys(e),i={},n=0;n<t.length;n++){var r=t[n];"function"==typeof e[r]&&(i[r]=e[r])}var a=Object.keys(i),c=void 0;try{o(i)}catch(e){c=e}return function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments[1];if(c)throw c;for(var n=!1,o={},r=0;r<a.length;r++){var l=a[r],h=i[l],u=e[l],d=h(u,t);if("undefined"==typeof d){var f=s(l,t);throw new Error(f)}o[l]=d,n=n||d!==u}return n?o:e}}t.__esModule=!0,t.default=r;var a=i(6),c=i(7),l=(n(c),i(22));n(l)},function(e,t){"use strict";function i(e){"undefined"!=typeof console&&"function"==typeof console.error&&console.error(e);try{throw new Error(e)}catch(e){}}t.__esModule=!0,t.default=i},function(e,t){"use strict";function i(e,t){return function(){return t(e.apply(void 0,arguments))}}function n(e,t){if("function"==typeof e)return i(e,t);if("object"!=typeof e||null===e)throw new Error("bindActionCreators expected an object or a function, instead received "+(null===e?"null":typeof e)+'. Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');for(var n=Object.keys(e),s={},o=0;o<n.length;o++){var r=n[o],a=e[r];"function"==typeof a&&(s[r]=i(a,t))}return s}t.__esModule=!0,t.default=n},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}function s(){for(var e=arguments.length,t=Array(e),i=0;i<e;i++)t[i]=arguments[i];return function(e){return function(i,n,s){var r=e(i,n,s),c=r.dispatch,l=[],h={getState:r.getState,dispatch:function(e){return c(e)}};return l=t.map(function(e){return e(h)}),c=a.default.apply(void 0,l)(r.dispatch),o({},r,{dispatch:c})}}}t.__esModule=!0;var o=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var i=arguments[t];for(var n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n])}return e};t.default=s;var r=i(25),a=n(r)},function(e,t){"use strict";function i(){for(var e=arguments.length,t=Array(e),i=0;i<e;i++)t[i]=arguments[i];return 0===t.length?function(e){return e}:1===t.length?t[0]:t.reduce(function(e,t){return function(){return e(t.apply(void 0,arguments))}})}t.__esModule=!0,t.default=i},function(e,t,i){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var s=i(5),o=i(27),r=n(o),a=i(28),c=n(a),l=i(29),h=n(l),u=i(30),d=n(u),f=(0,s.combineReducers)({items:r.default,groups:c.default,choices:h.default,general:d.default}),p=function(e,t){var i=e;return"CLEAR_ALL"===t.type&&(i=void 0),f(i,t)};t.default=p},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_ITEM":var n=[].concat(i(e),[{id:t.id,choiceId:t.choiceId,groupId:t.groupId,value:t.value,label:t.label,active:!0,highlighted:!1,customProperties:t.customProperties,placeholder:t.placeholder||!1,keyCode:null}]);return n.map(function(e){return e.highlighted&&(e.highlighted=!1),e});case"REMOVE_ITEM":return e.map(function(e){return e.id===t.id&&(e.active=!1),e});case"HIGHLIGHT_ITEM":return e.map(function(e){return e.id===t.id&&(e.highlighted=t.highlighted),e});default:return e}};t.default=n},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_GROUP":return[].concat(i(e),[{id:t.id,value:t.value,active:t.active,disabled:t.disabled}]);case"CLEAR_CHOICES":return e.groups=[];default:return e}};t.default=n},function(e,t){"use strict";function i(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t<e.length;t++)i[t]=e[t];return i}return Array.from(e)}Object.defineProperty(t,"__esModule",{value:!0});var n=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],t=arguments[1];switch(t.type){case"ADD_CHOICE":return[].concat(i(e),[{id:t.id,elementId:t.elementId,groupId:t.groupId,value:t.value,label:t.label||t.value,disabled:t.disabled||!1,selected:!1,active:!0,score:9999,customProperties:t.customProperties,placeholder:t.placeholder||!1,keyCode:null}]);case"ADD_ITEM":var n=e;return t.activateOptions&&(n=e.map(function(e){return e.active=t.active,e})),t.choiceId>-1&&(n=e.map(function(e){return e.id===parseInt(t.choiceId,10)&&(e.selected=!0),e})),n;case"REMOVE_ITEM":return t.choiceId>-1?e.map(function(e){return e.id===parseInt(t.choiceId,10)&&(e.selected=!1),e}):e;case"FILTER_CHOICES":var s=t.results,o=e.map(function(e){return e.active=s.some(function(t){return t.item.id===e.id&&(e.score=t.score,!0)}),e});return o;case"ACTIVATE_CHOICES":return e.map(function(e){return e.active=t.active,e});case"CLEAR_CHOICES":return e.choices=[];default:return e}};t.default=n},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{loading:!1},t=arguments[1];switch(t.type){case"LOADING":return{loading:t.isLoading};default:return e}};t.default=i},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.addItem=function(e,t,i,n,s,o,r,a){return{type:"ADD_ITEM",value:e,label:t,id:i,choiceId:n,groupId:s,customProperties:o,placeholder:r,keyCode:a}},t.removeItem=function(e,t){return{type:"REMOVE_ITEM",id:e,choiceId:t}},t.highlightItem=function(e,t){return{type:"HIGHLIGHT_ITEM",id:e,highlighted:t}},t.addChoice=function(e,t,i,n,s,o,r,a,c){return{type:"ADD_CHOICE",value:e,label:t,id:i,groupId:n,disabled:s,elementId:o,customProperties:r,placeholder:a,keyCode:c}},t.filterChoices=function(e){return{type:"FILTER_CHOICES",results:e}},t.activateChoices=function(){var e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];return{type:"ACTIVATE_CHOICES",active:e}},t.clearChoices=function(){return{type:"CLEAR_CHOICES"}},t.addGroup=function(e,t,i,n){return{type:"ADD_GROUP",value:e,id:t,active:i,disabled:n}},t.clearAll=function(){return{type:"CLEAR_ALL"}},t.setIsLoading=function(e){return{type:"LOADING",isLoading:e}}},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n=(t.capitalise=function(e){return e.replace(/\w\S*/g,function(e){return e.charAt(0).toUpperCase()+e.substr(1).toLowerCase()})},t.generateChars=function(e){for(var t="",i=0;i<e;i++){var n=a(0,36);t+=n.toString(36)}return t}),s=(t.generateId=function(e,t){var i=e.id||e.name&&e.name+"-"+n(2)||n(4);return i=i.replace(/(:|\.|\[|\]|,)/g,""),i=t+i},t.getType=function(e){return Object.prototype.toString.call(e).slice(8,-1)}),o=t.isType=function(e,t){var i=s(t);
+return void 0!==t&&null!==t&&i===e},r=(t.isNode=function(e){return"object"===("undefined"==typeof Node?"undefined":i(Node))?e instanceof Node:e&&"object"===("undefined"==typeof e?"undefined":i(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},t.isElement=function(e){return"object"===("undefined"==typeof HTMLElement?"undefined":i(HTMLElement))?e instanceof HTMLElement:e&&"object"===("undefined"==typeof e?"undefined":i(e))&&null!==e&&1===e.nodeType&&"string"==typeof e.nodeName},t.extend=function e(){for(var t={},i=arguments.length,n=function(i){for(var n in i)Object.prototype.hasOwnProperty.call(i,n)&&(o("Object",i[n])?t[n]=e(!0,t[n],i[n]):t[n]=i[n])},s=0;s<i;s++){var r=arguments[s];o("Object",r)&&n(r)}return t},t.whichTransitionEvent=function(){var e,t=document.createElement("fakeelement"),i={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(e in i)if(void 0!==t.style[e])return i[e]},t.whichAnimationEvent=function(){var e,t=document.createElement("fakeelement"),i={animation:"animationend",OAnimation:"oAnimationEnd",MozAnimation:"animationend",WebkitAnimation:"webkitAnimationEnd"};for(e in i)if(void 0!==t.style[e])return i[e]}),a=(t.getParentsUntil=function(e,t,i){for(var n=[];e&&e!==document;e=e.parentNode){if(t){var s=t.charAt(0);if("."===s&&e.classList.contains(t.substr(1)))break;if("#"===s&&e.id===t.substr(1))break;if("["===s&&e.hasAttribute(t.substr(1,t.length-1)))break;if(e.tagName.toLowerCase()===t)break}if(i){var o=i.charAt(0);"."===o&&e.classList.contains(i.substr(1))&&n.push(e),"#"===o&&e.id===i.substr(1)&&n.push(e),"["===o&&e.hasAttribute(i.substr(1,i.length-1))&&n.push(e),e.tagName.toLowerCase()===i&&n.push(e)}else n.push(e)}return 0===n.length?null:n},t.wrap=function(e,t){return t=t||document.createElement("div"),e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t.appendChild(e)},t.getSiblings=function(e){for(var t=[],i=e.parentNode.firstChild;i;i=i.nextSibling)1===i.nodeType&&i!==e&&t.push(i);return t},t.findAncestor=function(e,t){for(;(e=e.parentElement)&&!e.classList.contains(t););return e},t.findAncestorByAttrName=function(e,t){for(var i=e;i;){if(i.hasAttribute(t))return i;i=i.parentElement}return null},t.debounce=function(e,t,i){var n;return function(){var s=this,o=arguments,r=function(){n=null,i||e.apply(s,o)},a=i&&!n;clearTimeout(n),n=setTimeout(r,t),a&&e.apply(s,o)}},t.getElemDistance=function(e){var t=0;if(e.offsetParent)do t+=e.offsetTop,e=e.offsetParent;while(e);return t>=0?t:0},t.getElementOffset=function(e,t){var i=t;return i>1&&(i=1),i>0&&(i=0),Math.max(e.offsetHeight*i)},t.getAdjacentEl=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(e&&t){var n=e.parentNode.parentNode,s=Array.from(n.querySelectorAll(t)),o=s.indexOf(e),r=i>0?1:-1;return s[o+r]}},t.getScrollPosition=function(e){return"bottom"===e?Math.max((window.scrollY||window.pageYOffset)+(window.innerHeight||document.documentElement.clientHeight)):window.scrollY||window.pageYOffset},t.isInView=function(e,t,i){return this.getScrollPosition(t)>this.getElemDistance(e)+this.getElementOffset(e,i)},t.isScrolledIntoView=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:1;if(e){var n=void 0;return n=i>0?t.scrollTop+t.offsetHeight>=e.offsetTop+e.offsetHeight:e.offsetTop>=t.scrollTop}},t.stripHTML=function(e){var t=document.createElement("DIV");return t.innerHTML=e,t.textContent||t.innerText||""},t.addAnimation=function(e,t){var i=r(),n=function n(){e.classList.remove(t),e.removeEventListener(i,n,!1)};e.classList.add(t),e.addEventListener(i,n,!1)},t.getRandomNumber=function(e,t){return Math.floor(Math.random()*(t-e)+e)}),c=t.strToEl=function(){var e=document.createElement("div");return function(t){var i=t.trim(),n=void 0;for(e.innerHTML=i,n=e.children[0];e.firstChild;)e.removeChild(e.firstChild);return n}}();t.getWidthOfInput=function(e){var t=e.value||e.placeholder,i=e.offsetWidth;if(t){var n=c("<span>"+t+"</span>");if(n.style.position="absolute",n.style.padding="0",n.style.top="-9999px",n.style.left="-9999px",n.style.width="auto",n.style.whiteSpace="pre",document.body.contains(e)&&window.getComputedStyle){var s=window.getComputedStyle(e);s&&(n.style.fontSize=s.fontSize,n.style.fontFamily=s.fontFamily,n.style.fontWeight=s.fontWeight,n.style.fontStyle=s.fontStyle,n.style.letterSpacing=s.letterSpacing,n.style.textTransform=s.textTransform,n.style.padding=s.padding)}document.body.appendChild(n),t&&n.offsetWidth!==e.offsetWidth&&(i=n.offsetWidth+4),document.body.removeChild(n)}return i+"px"},t.sortByAlpha=function(e,t){var i=(e.label||e.value).toLowerCase(),n=(t.label||t.value).toLowerCase();return i<n?-1:i>n?1:0},t.sortByScore=function(e,t){return e.score-t.score},t.triggerEvent=function(e,t){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,n=new CustomEvent(t,{detail:i,bubbles:!0,cancelable:!0});return e.dispatchEvent(n)}},function(e,t){"use strict";!function(){function e(e,t){t=t||{bubbles:!1,cancelable:!1,detail:void 0};var i=document.createEvent("CustomEvent");return i.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),i}Array.from||(Array.from=function(){var e=Object.prototype.toString,t=function(t){return"function"==typeof t||"[object Function]"===e.call(t)},i=function(e){var t=Number(e);return isNaN(t)?0:0!==t&&isFinite(t)?(t>0?1:-1)*Math.floor(Math.abs(t)):t},n=Math.pow(2,53)-1,s=function(e){var t=i(e);return Math.min(Math.max(t,0),n)};return function(e){var i=this,n=Object(e);if(null==e)throw new TypeError("Array.from requires an array-like object - not null or undefined");var o,r=arguments.length>1?arguments[1]:void 0;if("undefined"!=typeof r){if(!t(r))throw new TypeError("Array.from: when provided, the second argument must be a function");arguments.length>2&&(o=arguments[2])}for(var a,c=s(n.length),l=t(i)?Object(new i(c)):new Array(c),h=0;h<c;)a=n[h],r?l[h]="undefined"==typeof o?r(a,h):r.call(o,a,h):l[h]=a,h+=1;return l.length=c,l}}()),Array.prototype.find||(Array.prototype.find=function(e){if(null==this)throw new TypeError("Array.prototype.find called on null or undefined");if("function"!=typeof e)throw new TypeError("predicate must be a function");for(var t,i=Object(this),n=i.length>>>0,s=arguments[1],o=0;o<n;o++)if(t=i[o],e.call(s,t,o,i))return t}),e.prototype=window.Event.prototype,window.CustomEvent=e}()}])});
+//# sourceMappingURL=choices.min.js.map
diff --git a/app/utils/static/image-loader.js b/app/utils/static/image-loader.js
new file mode 100644
index 0000000..121c3be
--- /dev/null
+++ b/app/utils/static/image-loader.js
@@ -0,0 +1,47 @@
+function add_images(){
+ var el = document.getElementById("id_body_markdown");
+ if (el){
+ var iframe = '<iframe frameborder="0" style="border: #dddddd 1px solid;margin-left: 20px;width:330px; height:720px;" src="/luximages/insert/?textarea='+el.id+'"></iframe>';
+ el.insertAdjacentHTML('afterend', iframe);
+ }
+
+ var featured_image = document.getElementById("id_featured_image")
+
+ if (featured_image) {
+ featured_image.querySelectorAll('li').forEach(function(element) {
+ var cur = element.dataset.imageid;
+ var loop = Number(element.dataset.loopcounter);
+ if (cur != "") {
+ if (loop <= 100) {
+ console.log(loop);
+ var request = new XMLHttpRequest();
+ request.open('GET', '/photos/luximage/data/admin/tn/'+cur+'/', true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ var el = element.getElementsByTagName('label')[0];
+ url = "url('"+data['url']+"');";
+ //console.log(url);
+ el.style.backgroundImage = 'url('+data["url"]+')';
+
+ //console.log(el.style);
+ } else {
+ console.log("server error", request.statusText);
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ }
+ }
+ });
+ }
+
+}
+document.addEventListener("DOMContentLoaded", function(event) {
+ add_images();
+ md = document.forms["entry_form"].elements["body_markdown"];
+ md.style.maxHeight = "300rem";
+ md.style.maxWidth = "300rem";
+});
diff --git a/app/utils/static/next-prev-links.js b/app/utils/static/next-prev-links.js
new file mode 100644
index 0000000..dddc879
--- /dev/null
+++ b/app/utils/static/next-prev-links.js
@@ -0,0 +1,60 @@
+function build_next_prev() {
+
+ var url = window.location.href
+ var cur = Number(url.split('/')[6]);
+ var app = url.split('/')[4];
+ var model = url.split('/')[5];
+ if (cur) {
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ style.innerHTML = '.np-container {padding-left: 0;} .prev, .next {display: inline-block; margin-right: .5em;} .prev:after { content: "|"; margin-left:.5em;} .prev a:before {content: "\u00AB"; margin-right: 3px;} .next a:after{content: "\u00BB"; margin-left: 3px;}';
+ document.getElementsByTagName('head')[0].appendChild(style);
+
+ json_url = '/admin/data/'+app+'/'+model+'/'+cur+'/';
+ //console.log(json_url);
+
+ var container = document.createElement("ul");
+ var next_li = document.createElement("li");
+ var next_link = document.createElement("a");
+ var prev_li = document.createElement("li");
+ var prev_link = document.createElement("a");
+ prev_li.className = "prev";
+ next_li.className = "next";
+ container.className = "np-container";
+ next_link.textContent = "Next";
+ prev_link.textContent = "Prev";
+
+ var request = new XMLHttpRequest();
+ request.open('GET', json_url, true);
+ request.onload = function() {
+ if (request.status >= 200 && request.status < 400) {
+ var data = JSON.parse(request.responseText);
+ next_link.href = data['next'];
+ prev_link.href = data['prev'];
+ if (data['next'] != '') {
+ next_li.appendChild(next_link);
+ }
+ if (data['prev']) {
+ prev_li.appendChild(prev_link);
+ }
+ } else {
+ console.log("server error");
+ }
+ };
+ request.onerror = function() {
+ console.log("error on request");
+ };
+ request.send();
+ container.appendChild(prev_li);
+ container.appendChild(next_li);
+ //console.log(container);
+ Array.from(document.getElementsByClassName('object-tools')).forEach(function(item) {
+ item.parentNode.insertBefore(container, item.nextSibling);
+ })
+ } else {
+ return;
+ }
+};
+document.addEventListener("DOMContentLoaded", function(event) {
+ build_next_prev();
+});
diff --git a/app/utils/urls.py b/app/utils/urls.py
new file mode 100644
index 0000000..7c37c5d
--- /dev/null
+++ b/app/utils/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+
+urlpatterns = [
+ path(
+ r'<str:app>/<str:model>/<int:pk>/',
+ views.nav_json,
+ name="admin_links"
+ ),
+]
diff --git a/app/utils/util.py b/app/utils/util.py
new file mode 100644
index 0000000..db6cf6a
--- /dev/null
+++ b/app/utils/util.py
@@ -0,0 +1,118 @@
+import re
+from django.apps import apps
+from django.template.loader import render_to_string
+from django.conf import settings
+from bs4 import BeautifulSoup
+import markdown
+
+
+def markdown_to_html(txt):
+ md = markdown.Markdown(
+ extensions=[
+ 'markdown.extensions.fenced_code',
+ 'markdown.extensions.codehilite',
+ 'markdown.extensions.attr_list',
+ 'footnotes',
+ 'extra'
+ ],
+ extension_configs = {
+ 'markdown.extensions.codehilite': {
+ 'css_class': 'highlight',
+ 'linenums': False
+ },
+ },
+ output_format='html5',
+ safe_mode=False
+ )
+ return md.convert(txt)
+
+
+def extract_main_image(markdown):
+ soup = BeautifulSoup(markdown, 'html.parser')
+ try:
+ image = soup.find_all('img')[0]['id']
+ img_pk = image.split('image-')[1]
+ return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk)
+ except IndexError:
+ return None
+
+
+def parse_image(s):
+ soup = BeautifulSoup(s.group(), "lxml")
+ for img in soup.find_all('img'):
+ try:
+ cl = img['class']
+ if cl[0] == 'postpic' or cl[0] == 'postpicright':
+ s = str(img).replace('[[base_url]]', settings.IMAGES_URL)
+ return s
+ else:
+ try:
+ image_id = img['id'].split("image-")[1]
+ i = apps.get_model('photos', 'LuxImage').objects.get(pk=image_id)
+ caption = False
+ exif = False
+ cluster_class = None
+ is_cluster = False
+ extra = None
+ if cl[0] == 'cluster':
+ css_class = cl[0]
+ is_cluster = True
+ cluster_class = cl[1]
+ try:
+ if cl[2] == 'caption':
+ caption = True
+ elif cl[2] == 'exif':
+ exif = True
+ else:
+ extra = cl[2]
+
+ if len(cl) > 3:
+ if cl[3] == 'exif':
+ exif = True
+ except:
+ pass
+ elif cl[0] != 'cluster' and len(cl) > 1:
+ css_class = cl[0]
+ if cl[1] == 'caption':
+ caption = True
+ if cl[1] == 'exif':
+ exif = True
+ elif cl[0] != 'cluster' and len(cl) > 2:
+ css_class = cl[0]
+ if cl[1] == 'caption':
+ caption = True
+ if cl[2] == 'exif':
+ exif = True
+ print('caption'+str(caption))
+ else:
+ css_class = cl[0]
+ return render_to_string("lib/img_%s.html" % css_class, {'image': i, 'caption': caption, 'exif': exif, 'is_cluster': is_cluster, 'cluster_class': cluster_class, 'extra': extra})
+ except KeyError:
+ ''' regular inline image, not a luximage '''
+ return str(img)
+ except KeyError:
+ ''' regular inline image, not a luximage '''
+ return str(img)
+
+
+def render_images(s):
+ s = re.sub('<img(.*)/>', parse_image, s)
+ return s
+
+
+def parse_video(s):
+ soup = BeautifulSoup(s, "lxml")
+ if soup.find('video'):
+ return True
+ return False
+
+def parse_reg_bio_page():
+ content = requests.get("https://www.theregister.co.uk/Author/Scott-Gilbertson/")
+ soup = BeautifulSoup(content, 'html.parser')
+ try:
+ image = soup.find_all('img')[0]['id']
+ img_pk = image.split('image-')[1]
+ return apps.get_model('photos', 'LuxImage').objects.get(pk=img_pk)
+ except IndexError:
+ return None
+
diff --git a/app/utils/views.py b/app/utils/views.py
new file mode 100644
index 0000000..7a0c67d
--- /dev/null
+++ b/app/utils/views.py
@@ -0,0 +1,102 @@
+from itertools import chain
+import json
+from django.http import Http404, HttpResponse, JsonResponse
+from django.apps import apps
+from django.views.generic import ListView
+from django.views.generic.base import View, RedirectView
+from django.views.generic.edit import FormView, ModelFormMixin
+from django.utils.decorators import method_decorator
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import render_to_response
+from django.shortcuts import render
+from django.template import RequestContext
+from taggit.models import Tag
+#from dal import autocomplete
+
+
+class PaginatedListView(ListView):
+ """
+ handles my own pagination system
+ """
+ context_object_name = 'object_list'
+
+ def dispatch(self, request, *args, **kwargs):
+ path = request.path.split('/')[1:-1]
+ if path[-1] == self.kwargs['page']:
+ path = "/".join(t for t in path[:-1])
+ request.page_url = "/" + path + '/%d/'
+ else:
+ request.page_url = request.path + '%d/'
+ print(request.page_url)
+ request.page = int(self.kwargs['page'])
+ request.base_path = path
+ return super(PaginatedListView, self).dispatch(request, *args, **kwargs)
+
+
+@method_decorator(login_required, name='dispatch')
+class LoggedInViewWithUser(View):
+
+ def get_form_kwargs(self, **kwargs):
+ kwargs = super().get_form_kwargs(**kwargs)
+ kwargs.update({'user': self.request.user})
+ return kwargs
+
+
+@method_decorator(login_required, name='dispatch')
+class LoggedInViewWithUser(FormView):
+
+ def get_form_kwargs(self, **kwargs):
+ kwargs = super().get_form_kwargs(**kwargs)
+ kwargs.update({'user': self.request.user})
+ return kwargs
+
+
+class AjaxableResponseMixin:
+ """
+ Mixin to add AJAX support to a form.
+ Must be used with an object-based FormView (e.g. CreateView)
+ """
+ def form_invalid(self, form):
+ response = super().form_invalid(form)
+ if self.request.is_ajax():
+ return JsonResponse(form.errors, status=400)
+ else:
+ return response
+
+ def form_valid(self, form):
+ # We make sure to call the parent's form_valid() method because
+ # it might do some processing (in the case of CreateView, it will
+ # call form.save() for example).
+ print(self.request.META)
+ print('x request header', self.request.META['HTTP_X_REQUESTED_WITH'])
+ response = super().form_valid(form)
+ if self.request.is_ajax():
+ data = {
+ 'pk': self.object.pk,
+ }
+ return JsonResponse(data)
+ else:
+ return response
+'''
+class TagAutocomplete(autocomplete.Select2QuerySetView):
+ def get_queryset(self):
+ # Don't forget to filter out results depending on the visitor !
+ if not self.request.user.is_authenticated:
+ return Tag.objects.none()
+
+ qs = Tag.objects.all()
+
+ if self.q:
+ qs = qs.filter(name__istartswith=self.q)
+
+ return qs
+'''
+
+def nav_json(request, app, model, pk):
+ model = apps.get_model(app_label=app, model_name=model)
+ p = model.objects.get(pk=pk)
+ data = {}
+ data['prev'] = p.get_previous_admin_url
+ data['next'] = p.get_next_admin_url
+ data = json.dumps(data)
+ return HttpResponse(data)
diff --git a/app/utils/widgets.py b/app/utils/widgets.py
new file mode 100644
index 0000000..6dc28bf
--- /dev/null
+++ b/app/utils/widgets.py
@@ -0,0 +1,153 @@
+import os
+from django import forms
+from django.contrib import admin
+from django.contrib.admin.widgets import AdminFileWidget
+from django.utils.safestring import mark_safe
+from django.forms import widgets
+from django.urls import reverse
+from django.utils.translation import ugettext_lazy as _
+from django.template.loader import render_to_string
+from django.template import Context
+from django.forms.widgets import SelectMultiple
+from django.conf import settings
+
+import markdown
+
+from bs4 import BeautifulSoup
+from django.utils.module_loading import import_string
+
+
+class CustomSelectMultiple(SelectMultiple):
+ def render_options(self, choices, selected_choices):
+ if not selected_choices:
+ # there is CreatView and we have no selected choices - render all selected
+ render_option = self.render_option
+ else:
+ # there is UpdateView and we have selected choices - render as default
+ render_option = super(CustomSelectMultiple, self).render_option
+
+ selected_choices = set(force_text(v) for v in selected_choices)
+ output = []
+ for option_value, option_label in chain(self.choices, choices):
+ if isinstance(option_label, (list, tuple)):
+ output.append(format_html('<optgroup label="{0}">', force_text(option_value)))
+ for option in option_label:
+ output.append(render_option(selected_choices, *option))
+ output.append('</optgroup>')
+ else:
+
+ output.append(render_option(selected_choices, option_value, option_label))
+ return '\n'.join(output)
+
+ def render_option(self, selected_choices, option_value, option_label):
+ option_value = force_text(option_value)
+ selected_html = mark_safe(' selected="selected"')
+
+ return format_html('<option value="{0}"{1}>{2}</option>',
+ option_value,
+ selected_html,
+ force_text(option_label))
+
+
+class TagListFilter(admin.SimpleListFilter):
+ # Human-readable title which will be displayed in the
+ # right admin sidebar just above the filter options.
+ title = _('tag')
+
+ # Parameter for the filter that will be used in the URL query.
+ parameter_name = 'tag'
+
+ def lookups(self, request, model_admin):
+ """
+ Returns a list of tuples. The first element in each
+ tuple is the coded value for the option that will
+ appear in the URL query. The second element is the
+ human-readable name for the option that will appear
+ in the right sidebar.
+ """
+ tl = []
+ self.model_to_use = model_admin.model
+ for t in self.model_to_use.tags.all().order_by('name'):
+ tl += (t.name, t.name),
+ return tl
+
+ def queryset(self, request, queryset):
+ """
+ Returns the filtered queryset based on the value
+ provided in the query string and retrievable via
+ `self.value()`.
+ """
+ qs = self.model_to_use.objects.all()
+ try:
+ request.GET['tag']
+ return qs.filter(tags__name=self.value())
+ except:
+ return qs
+
+
+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 ImageRadioSelect(forms.RadioSelect):
+ template_name = 'horizontal_select.html'
+
+
+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))
+
+
+class LGEntryForm(forms.ModelForm):
+ class Meta:
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 40, 'cols': 100}),
+ 'featured_image': ImageRadioSelect,
+ }
+
+
+class LGEntryFormSmall(forms.ModelForm):
+ class Meta:
+ widgets = {
+ 'body_markdown': forms.Textarea(attrs={'rows': 12, 'cols': 100}),
+ }
+
+
+class RelatedFieldWidgetCanAdd(widgets.Select):
+ """
+ Modifies standard django Select widget to add link after to add new instance
+ of related model (doesn't check permissions, that's for the form instance)
+ """
+ def __init__(self, related_model, related_url=None, *args, **kw):
+ super(RelatedFieldWidgetCanAdd, self).__init__(*args, **kw)
+ if not related_url:
+ rel_to = related_model
+ info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
+ related_url = 'admin:%s_%s_add' % info
+ print(related_url)
+ self.related_url = related_url
+
+ def render(self, name, value, *args, **kwargs):
+ self.related_url = reverse(self.related_url)
+ print(self.related_url)
+ output = [super(RelatedFieldWidgetCanAdd, self).render(name, value, *args, **kwargs)]
+ output.append('<a class="circle plus small-circle modal-open" href="%s" id="add_id_%s" data-modal-hed-class="%s" data-modal-hed="Add a New %s">New</a>' % (self.related_url, name, name, name.capitalize()))
+ return mark_safe(u''.join(output))
diff --git a/config/__init__.py b/config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/config/__init__.py
diff --git a/config/base_urls.py b/config/base_urls.py
new file mode 100644
index 0000000..ed7da2d
--- /dev/null
+++ b/config/base_urls.py
@@ -0,0 +1,35 @@
+from django.urls import path, re_path, include
+from django.contrib import admin
+from django.conf.urls.static import static
+from django.conf import settings
+from django.contrib.sitemaps.views import sitemap
+
+from pages.views import PageDetailView, HomePageDetailView
+#import builder.views
+import utils.views
+import builder.views
+
+
+admin.autodiscover()
+
+urlpatterns = [
+ re_path(r'^admin/build/.*', builder.views.do_build),
+ path(r'admin/data/', include('utils.urls')),
+ path(r'admin/', admin.site.urls),
+ #path(r'luximages/insert/', utils.views.insert_image),
+ #path(r'sitemap.xml', sitemap, {'sitemaps': sitemaps}),
+ path(r'<slug>', PageDetailView.as_view()),
+ path(r'<path>/<slug>/', PageDetailView.as_view()),
+ path(r'', HomePageDetailView.as_view()),
+] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+
+
+if settings.DEBUG:
+ import debug_toolbar
+ urlpatterns = [
+ path('__debug__/', include(debug_toolbar.urls)),
+
+ # For django versions before 2.0:
+ # url(r'^__debug__/', include(debug_toolbar.urls)),
+
+ ] + urlpatterns
diff --git a/config/django.ini b/config/django.ini
new file mode 100644
index 0000000..f238c17
--- /dev/null
+++ b/config/django.ini
@@ -0,0 +1,30 @@
+# django.ini file
+[uwsgi]
+
+# maximum number of processes
+processes = 4
+max-requests = 5000
+enable-threads = true
+# the socket (use the full path to be safe)
+uid = http
+gid = http
+
+socket = /tmp/uwsgi.sock
+chmod-socket = 664
+chown-socket = www-data:www-data
+
+# the base directory
+chdir = /home/lxf/sites/luxagraf.net
+
+# django's wsgi file
+module = config.wsgi
+# the virtualenv
+home = /home/lxf/sites/luxagraf.net/venv
+
+buffer-size =65535
+#plugin=python
+limit-as = 1024
+limit-post = 0
+# clear environment on exit
+vacuum = true
+logto = /var/log/uwsgi/test.log
diff --git a/config/requirements.txt b/config/requirements.txt
new file mode 100644
index 0000000..a75a0c8
--- /dev/null
+++ b/config/requirements.txt
@@ -0,0 +1,41 @@
+backcall==0.1.0
+beautifulsoup4==4.7.1
+bs4==0.0.1
+certifi==2019.3.9
+chardet==3.0.4
+decorator==4.4.0
+Django==2.1.7
+django-debug-toolbar==1.11
+django-extensions==2.1.6
+django-taggit==1.1.0
+django-typogrify==1.3.3
+idna==2.8
+ipython==7.4.0
+ipython-genutils==0.2.0
+jedi==0.13.3
+Jinja2==2.10
+lxml==4.3.3
+Markdown==3.1
+MarkupSafe==1.1.1
+parso==0.3.4
+pexpect==4.6.0
+pickleshare==0.7.5
+Pillow==5.4.1
+pkg-resources==0.0.0
+prompt-toolkit==2.0.9
+psycopg2-binary==2.7.7
+ptyprocess==0.6.0
+pwned-passwords-django==1.3.1
+Pygments==2.3.1
+python-decouple==3.1
+python-resize-image==1.1.18
+pytz==2018.9
+requests==2.21.0
+six==1.12.0
+smartypants==2.0.1
+soupsieve==1.9
+sqlparse==0.3.0
+traitlets==4.3.2
+typogrify==2.0.7
+urllib3==1.24.1
+wcwidth==0.1.7
diff --git a/config/wsgi.py b/config/wsgi.py
new file mode 100644
index 0000000..cc7e144
--- /dev/null
+++ b/config/wsgi.py
@@ -0,0 +1,26 @@
+"""
+WSGI config for myproject project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
+"""
+
+import os, sys, site
+from os.path import dirname, abspath
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+
+# Fix markdown.py (and potentially others) using stdout
+sys.stdout = sys.stderr
+SERVER_ROOT = abspath(dirname(dirname(__file__)))+'/'
+# Tell wsgi to add the Python site-packages to it's path.
+site.addsitedir(SERVER_ROOT+'venv/lib/python3.6/site-packages')
+sys.path = [SERVER_ROOT,] + sys.path
+sys.path.insert(0, os.path.join(SERVER_ROOT, "app"))
+sys.path.insert(0, os.path.join(SERVER_ROOT, "app/lib"))
+sys.path.insert(0, os.path.join(SERVER_ROOT, "config"))
+
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
+
diff --git a/design/config.rb b/design/config.rb
new file mode 100644
index 0000000..3d2c82a
--- /dev/null
+++ b/design/config.rb
@@ -0,0 +1,12 @@
+project_type = :stand_alone
+# Set this to the root of your project when deployed:
+http_path = "/"
+css_dir = "../site/media"
+sass_dir = "sass"
+images_dir = "../site/media/img"
+javascripts_dir = "../site/media"
+#output_style = :compressed
+output_style = :compressed
+#output_style = (environment == :production) ? :compressed : :expanded
+# To enable relative paths to assets via compass helper functions. Uncomment:
+# relative_assets = true
diff --git a/design/sass/_content.scss b/design/sass/_content.scss
new file mode 100644
index 0000000..9a81b64
--- /dev/null
+++ b/design/sass/_content.scss
@@ -0,0 +1,236 @@
+.hero-wrapper {
+ @include fancy_sans;
+ padding: 5rem 0 0;
+ &:after {
+ @include faded_line_after;
+ margin-top: 4rem;
+ margin-bottom: 4rem;
+ padding: 0;
+ }
+}
+.circle-pic {
+ border-radius: 50%;
+ border: 5px solid #000;
+}
+.btn {
+ @include fontsize(14);
+ display: inline-block;
+ border-radius: 4px;
+ @include fancy_sans;
+ @include smcaps;
+ -webkit-appearance: none;
+ text-decoration: none;
+ cursor: pointer;
+ background: $link_color;
+ color: #fff !important;
+ border: 1px solid $link_color;
+ padding: 7px 9px;
+ white-space: nowrap;
+ &:hover {
+ background: $link_hover_color;
+ border: 1px solid $link_hover_color;
+ }
+}
+.btn-small {
+ @include fontsize(10);
+ @include smcaps;
+}
+.btn-hollow {
+ @include fontsize(17);
+ padding: 6px 8px;
+ border: none; //1px solid $body_font_light;
+ color: $link_color !important;
+ outline: $link_color !important;
+ background: white;
+ border: 1px solid $link_color;
+ &:hover {
+ background: $link_color !important;
+ color: white !important;
+ }
+}
+.hed-alpha {
+ @include fancy_serif;
+ line-height: 1.2;
+ font-weight: 600;
+}
+.hed-beta {
+ line-height: 1.4;
+}
+.hed-gamma {
+ line-height: 1.4;
+ @include fontsize(20);
+ @include fancy_sans;
+ text-align: center;
+ letter-spacing: 1px;
+ margin: 0 auto;
+}
+.hed-delta {
+ @include fancy_sans;
+ @include fontsize(22);
+ font-weight: bold;
+ margin-bottom: .5em;
+}
+.hed-epsilon {
+ @include fontsize(14);
+ @include fancy_sans;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ font-weight: bold;
+ color: $body_font_light;
+ line-height: 1.2;
+ text-align: center;
+ margin: 0;
+}
+.hed-border {
+ display: inline-block;
+ border-bottom: 3px solid #efefef;
+ width: auto;
+}
+.hero {
+ .hero-pic {
+ max-width: 50%;
+ margin: 1.5rem auto;
+ }
+ h3.hed-alpha {
+ @include fontsize(24);
+ }
+ .hed-beta {
+ @include fancy_sans;
+ @include fontsize(22);
+ margin: .5rem auto;
+ text-align: center;
+ &:last-of-type:after {
+ @include faded_line_after;
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ }
+ }
+ @include breakpoint(beta) {
+ display: flex;
+ align-items: flex-start;
+ align-content: flex-start;
+ .hero-text {
+ margin-left: 5rem;
+ & > * {
+ text-align: left;
+ }
+ }
+ p {
+ width: 90%;
+ @include fontsize(20);
+ }
+ .btn {
+ margin-top: 1.5rem;
+ }
+ }
+}
+.home {
+ h4 {
+ @include fancy_sans;
+ text-align: center;
+ letter-spacing: 1px;
+ }
+ hr {
+ border-top: 3px solid #efefef;
+ margin: 5em auto;
+ width: 40%;
+ }
+}
+.border-wrapper {
+ margin: 2rem auto 0;
+}
+.home-logos {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ li {
+ margin: 0 .5rem;
+ }
+ img {
+ max-width: 80px;
+ }
+ @include breakpoint(gamma) {
+ @include constrain_wide;
+ margin: 0 auto;
+ img {
+ max-width: 120px;
+ }
+ }
+}
+
+
+.home-border-wrapper {
+ @include breakpoint(beta) {
+ margin-top: 4rem;
+ .btn {
+ margin-bottom: 4rem;
+ }
+ }
+ &:before {
+ @include faded_line_after;
+ margin-top: 0;
+ margin-bottom: 5rem;
+ }
+}
+
+.card-image {
+ max-height: 10rem;
+ overflow: hidden;
+ border: 4px $body_font_color solid;
+ img {
+ width: 100%;
+ margin-top: -14%;
+ }
+}
+.home-card-wrapper {
+ @include breakpoint(beta) {
+ margin-top: 4rem;
+ .btn {
+ margin-bottom: 4rem;
+ }
+ }
+ &:after {
+ @include faded_line_after;
+ margin-top: 0;
+ margin-bottom: 5rem;
+ }
+}
+.card {
+ margin: 4rem auto;
+ p {
+ margin-top: .5rem;
+ @include fontsize(18);
+ }
+ h5 {
+ @include fontsize(24);
+ text-align: center;
+ margin: 1rem 0 0 0;
+ }
+ &:after {
+ @include faded_line_after;
+ }
+ &:last-of-type:after {
+ display: none;
+ }
+ @include breakpoint(beta) {
+ flex-grow: 1;
+ flex-shrink: 1;
+ flex-basis: 0;
+ margin-top: 0;
+ margin-right: 2rem;
+ &:last-of-type {
+ margin-right: 0;
+ }
+ &:after {
+ display: none;
+ }
+ }
+}
+.flex {
+ @include breakpoint(gamma) {
+ display: flex;
+ justify-content: center ;
+ }
+}
diff --git a/design/sass/_fonts.scss b/design/sass/_fonts.scss
new file mode 100644
index 0000000..77f6f7c
--- /dev/null
+++ b/design/sass/_fonts.scss
@@ -0,0 +1,35 @@
+@font-face {
+ font-family: 'carrois_gothicregular';
+ src: url('/media/fonts/carroisgothic-regular-webfont.eot');
+ src: url('/media/fonts/carroisgothic-regular-webfont.eot?#iefix') format('embedded-opentype'),
+ url('/media/fonts/carroisgothic-regular-webfont.woff') format('woff'),
+ url('/media/fonts/carroisgothic-regular-webfont.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'mffweb';
+ src: url('/media/fonts/ffmpb.woff2') format('woff2');
+ src: url('/media/fonts/ffmpb.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: 'mffweb';
+ src: url('/media/fonts/ffmbi.woff2') format('woff2');
+ src: url('/media/fonts/ffmbi.woff') format('woff');
+ font-weight: 400;
+ font-style: italic;
+}
+
+
+@font-face {
+ font-family: 'mffnweb';
+ src: url('/media/fonts/ffmn.woff2') format('woff2');
+ src: url('/media/fonts/ffmn.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+}
+
diff --git a/design/sass/_footer.scss b/design/sass/_footer.scss
new file mode 100644
index 0000000..1ff56b9
--- /dev/null
+++ b/design/sass/_footer.scss
@@ -0,0 +1,54 @@
+footer {
+ margin-top: 5em;
+ @include breakpoint(gamma) {
+ max-width: 960;
+ }
+ &:before {
+ @include breakpoint(beta) {
+ @include faded_line_after;
+ margin-bottom: 1.2em;
+ }
+ }
+ .footer-nav {
+ list-style-type: none !important;
+ margin-left: 0 !important;
+ border-top: 1px $body_font_color dotted;
+ border-bottom: 1px $body_font_color dotted;
+ padding: .5rem 0;
+ @include breakpoint(beta) {
+ border: none;
+ }
+ }
+ li {
+ display: inline;
+ margin: 0 .25em;
+ &:after {
+ content: "\00b7";
+ color: #999999;
+ padding-left: 0.75em;
+ }
+ a {
+ color: $secondary-link-color;
+ text-decoration: none;
+ }
+ ul { display:inline;}
+ }
+ li:last-of-type {
+ margin-right: 0;
+ &:after {
+ content: " ";
+ }
+ }
+ p {
+ @include fontsize(10);
+ text-align: center;
+ margin-top: 1.5em;
+ margin-bottom: 1.5em;
+ }
+}
+#license {
+ @include fancy_sans;
+ @include fontsize(12);
+ text-transform: none;
+ letter-spacing: normal;
+}
diff --git a/design/sass/_global.scss b/design/sass/_global.scss
new file mode 100644
index 0000000..719d23a
--- /dev/null
+++ b/design/sass/_global.scss
@@ -0,0 +1,243 @@
+html {
+ border-top: 0.25em solid $body_font_color;
+}
+
+body {
+ margin: 0 auto;
+ padding: 0;
+ overflow-x: hidden;
+ font:$body_p_font;
+ color: $body_font_color;
+ text-align: center;
+ background-color: transparent
+}
+ul {
+ padding: 0;
+}
+// eliminate touch delay on mobile safari
+a, button, input, select, textarea, label, summary {
+ touch-action: manipulation;
+}
+a {
+ color: $body_font_color;
+ -webkit-transition: all 0.1s ease;
+ -moz-transition: all 0.1s ease;
+ -ms-transition: all 0.1s ease;
+ transition: all 0.1s ease;
+ text-decoration-color: $link_color;
+ &:hover {
+ text-decoration: none;
+ }
+ &:visited {
+ color: $body_font_color;
+ }
+}
+p {
+ @include fancy_serif;
+ @include fontsize(20);
+ text-align: left;
+ @include breakpoint(alpha){
+ @include fontsize(22);
+ line-height: 1.5;
+ }
+ @include breakpoint(beta){
+ @include fontsize(24);
+ line-height: 1.6;
+ }
+}
+time {
+ @include smcaps;
+ @include fontsize(11);
+ display: block;
+ span {
+ @include fontsize(13);
+ }
+}
+abbr {
+ cursor: help;
+}
+pre {
+ text-align: left;
+ @include breakpoint(alpha){
+ @include fontsize(18);
+ line-height: 1.5;
+ }
+}
+object, embed, video {
+ max-width: 100%;
+ width: 100%;
+ height: auto;
+}
+audio {
+ max-width: 100%;
+ width: 100%;
+}
+blockquote {
+ @include fontsize(18);
+ display: block;
+ border-top: 4px solid lighten($body_font_light, 20);
+ border-bottom: 4px solid lighten($body_font_light, 20);
+ margin: 3rem 0;
+ position: relative;
+ text-align: left;
+ font-style: italic;
+ cite {
+ display: block;
+ text-align: right;
+ }
+ @include breakpoint(alpha){
+ @include fontsize(20);
+ line-height: 1.5;
+ }
+ @include breakpoint(beta){
+ @include fontsize(22);
+ line-height: 1.6;
+ }
+}
+blockquote:before {
+ @include fancy_sans;
+ @include fontsize(68);
+ content: '\201C';
+ position: absolute;
+ top: -1.35rem;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 3rem;
+ height: 2rem;
+ color: #666;
+ text-align: center;
+}
+hr {
+ border: 0;
+ height: 1px;
+ @include faded_line_after;
+ margin: 3rem 0;
+}
+img {
+ max-width: 100%;
+}
+figure {
+ margin: 0;
+}
+figcaption {
+ text-align: left;
+}
+figcaption, figcaption a {
+ @include fancy_sans;
+ @include fontsize(16);
+ text-align: left;
+ line-height: 1.9;
+ padding: .3rem .5rem .3rem 0;
+ color: #666;
+ border-bottom: 1px lighten($body_font_light, 20) solid;
+ margin-bottom: 1rem;
+}
+figcaption a:visited {
+ color: #666;
+}
+h1 {
+ @include fancy_serif;
+ @include fontsize(36);
+ font-weight: normal;
+}
+h2 {
+ @include fancy_serif;
+ @include fontsize(28);
+ font-weight: normal;
+ text-align: left;
+ @include breakpoint(gamma){
+ @include fontsize(30);
+ line-height: 1.6;
+ }
+}
+h3 {
+ @include fancy_sans;
+ @include fontsize(24);
+ text-align: left;
+ @include breakpoint(gamma){
+ @include fontsize(28);
+ line-height: 1.4;
+ }
+}
+h4 {
+ @include fontsize(20);
+ text-align: left;
+ @include breakpoint(gamma){
+ @include fontsize(22);
+ line-height: 1.4;
+ }
+}
+h5 {
+ @include fontsize(16);
+ text-align: left;
+ @include breakpoint(gamma){
+ @include fontsize(18);
+ line-height: 1.4;
+ }
+}
+.subhead {
+ font-size: 26px !important;
+ font-style: italic;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+.subhead + p {
+ margin-top: .75rem !important;
+}
+.essay-intro .subhead + p:first-of-type {
+ margin-top: .75rem !important;
+}
+//************** Universals ************************
+.hide {
+ display: none;
+}
+.strike {
+ text-decoration: line-through;
+}
+.yes {
+ background: green !important;
+ color: white;
+}
+.no {
+ background: red !important;
+ color: white;
+}
+.alert {
+ color: red !important;
+}
+.small {
+ font-size: 90%;
+}
+.content {
+ @include constrain_wide;
+}
+.narrow {
+ @include constrain_narrow;
+}
+//**************** Page Breadcrumbs ************************
+
+#breadcrumbs {
+ @include constrain_wide();
+ padding: 0;
+ list-style-type: none;
+ text-align: center;
+ li {
+ display: inline;
+ }
+ a {
+ color: $secondary-link-color;
+ }
+ @include breakpoint(gamma) {
+ text-align: left;
+ }
+}
+
+
+.list-inline {
+ @extend %clearfix;
+ list-style-type: none;
+ padding: none;
+ li {
+ margin: 1rem;
+ }
+}
diff --git a/design/sass/_header.scss b/design/sass/_header.scss
new file mode 100644
index 0000000..431897e
--- /dev/null
+++ b/design/sass/_header.scss
@@ -0,0 +1,135 @@
+.header-wrapper {
+}
+
+#logo {
+ a {
+ @include fontsize(32);
+ line-height: 1;
+ text-decoration: none;
+ display: block;
+ font-weight: 300;
+ font-family: 'carrois_gothicregular', Helvetica, sans-serif;
+ color: $body_font_color;
+ }
+ .tagline {
+ display: block;
+ @include fancy_sans;
+ @include smcaps;
+ @include fontsize(12);
+ font-style: normal;
+ margin-left: 2px;
+ }
+}
+.site-banner {
+ @extend %clearfix;
+ margin: 0 auto;
+ @include constrain_wide;
+ @include smcaps;
+ nav {
+ border-top: 1px #444444 dotted;
+ border-bottom: 1px #444444 dotted;
+ //box-shadow: 0 3px 8px 0 #e6e6e6
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-right: -20px;
+ margin-left: -20px;
+ margin-top: 1em;
+ padding: 0.25em 0.5em;
+ a {
+ text-decoration: none;
+ color: #505050;
+ &:visited {
+ color: #505050
+ }
+ }
+ ul {
+ @include smcaps;
+ @include fancy_sans;
+ @include fontsize(15);
+ max-width: 100%;
+ font-weight: 600;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em !important;
+ padding: 0;
+ @include constrain(85%);
+ }
+ li {
+ display: inline;
+ margin: 0 0.25em;
+ &:after {
+ content: "\00b7";
+ color: #999999;
+ padding-left: 0.75em;
+ }
+ &:last-of-type {
+ margin-right: 0;
+ &:after {
+ content: " ";
+ }
+ }
+ }
+ @include breakpoint(beta) {
+ }
+ }
+ @include breakpoint(beta) {
+ height: 90px;
+ position: relative;
+ #logo {
+ float: left;
+ position: absolute;
+ bottom: 28%;
+ text-align: left;
+ a {
+ width: 360px;
+ &:before {
+ display: inline-block;
+ background-size: 102px;
+ //if sox
+ #background: url("img/soxlogo.svg") center bottom no-repeat;
+ #background-size: 70px;
+ height: 85px;
+ width: 105px;
+ //if sox
+ #width: 80px;
+ margin-right: 10px;
+ }
+ }
+ .sitesubtitle {
+ margin-left: 116px;
+ //if sox
+ #margin-left: 90px;
+ margin-top: -30px;
+ }
+ }
+ nav {
+ float: right;
+ border: none;
+ margin: 22px 0 0 0;
+ padding: 0;
+ ul {
+ max-width: 50em;
+ }
+ }
+ }
+}
+
+.header-wrapper {
+ @extend %clearfix;
+ @include breakpoint(beta) {
+ border-bottom: 1px #f3efef solid;
+ position: relative;
+ }
+ @include breakpoint(gamma) {
+ max-width: $breakpoint-gamma;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ @include breakpoint(delta) {
+ margin-top: 1.3rem;
+ max-width: $breakpoint-delta;
+ }
+ @include breakpoint(epsilon) {
+ margin-top: 1.3rem;
+ max-width: $max_width;
+ }
+}
diff --git a/design/sass/_mixins.scss b/design/sass/_mixins.scss
new file mode 100644
index 0000000..f12888e
--- /dev/null
+++ b/design/sass/_mixins.scss
@@ -0,0 +1,107 @@
+
+$body_p_font: normal 100% / 1.5 Georgia, Cambria, "Times New Roman", Times, serif;
+$body_font_color: #222;
+$link_color: #b53a04;
+$body_font_light: #787474;
+$secondary-link-color: #838383;
+$narrow-beta-width: 720px;
+$narrow-max-width: 750px;
+$max_width: 1140px;
+$link_hover_color: #b53a04;
+
+$home-palette-primary: #14cfe7;
+
+@mixin smcaps {
+ @include fancy_sans;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+@function calc-rem($size) {
+ $remsize: $size/16;
+ @return #{$remsize}rem;
+}
+@mixin fontsize($size) {
+ font-size: $size + px;
+ font-size: calc-rem($size);
+}
+
+@mixin generic-sans {
+ font-family: sans-serif;
+}
+@mixin generic-serif {
+ font-family: Georgia, 'Times New Roman', serif;
+}
+@mixin fancy-sans {
+ font-family: mffnweb, Helvetica, sans-serif;
+}
+@mixin fancy-serif {
+ font-family: mffweb, Georgia, 'Times New Roman', serif;
+}
+%clearfix {
+ *zoom: 1;
+ &:before {
+ content: " ";
+ display: table;
+ }
+ &:after {
+ content: " ";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin transparent_class {
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
+ filter: alpha(opacity = 90);
+ opacity: 0.9;
+}
+
+@mixin faded_line_after {
+ display: block;
+ content: "";
+ margin-top: 3em;
+ height: 1px;
+ width: 100%;
+ background: -webkit-linear-gradient(left, rgba(0,0,0,0),rgba(0,0,0,0.1),rgba(0,0,0,0));
+ background: -moz-linear-gradient(left, rgba(0,0,0,0),rgba(0,0,0,0.1),rgba(0,0,0,0));
+ background: -o-linear-gradient(left, rgba(0,0,0,0),rgba(0,0,0,0.1),rgba(0,0,0,0));
+ background: linear-gradient(left, rgba(0,0,0,0),rgba(0,0,0,0.1),rgba(0,0,0,0));
+}
+
+//generic constrain function
+@mixin constrain($size) {
+ max-width: $size;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+//set an element to centered, narrowish column width
+//used mostly on columns of text
+@mixin constrain_narrow() {
+ max-width: 94%;
+ margin-left: auto;
+ margin-right: auto;
+ @include breakpoint(beta) {
+ max-width: $narrow-beta-width;
+ }
+ @include breakpoint(gamma) {
+ max-width: $narrow-max-width;
+ }
+}
+
+//set an element to centered, wideish column width
+@mixin constrain_wide() {
+ max-width: 94%;
+ margin-left: auto;
+ margin-right: auto;
+ @include breakpoint(gamma) {
+ max-width: $breakpoint-gamma;
+ }
+ @include breakpoint(delta) {
+ max-width: $breakpoint-delta;
+ }
+ @include breakpoint(epsilon) {
+ max-width: $max_width;
+ }
+}
+
diff --git a/design/sass/_queries.scss b/design/sass/_queries.scss
new file mode 100644
index 0000000..5b7dadc
--- /dev/null
+++ b/design/sass/_queries.scss
@@ -0,0 +1,23 @@
+$breakpoint-alpha: 420px;
+$breakpoint-beta: 728px;
+$breakpoint-gamma: 824px;
+$breakpoint-delta: 960px;
+$breakpoint-epsilon: $max_width;
+
+@mixin breakpoint($point) {
+ @if $point == "alpha" {
+ @media screen and (min-width:$breakpoint-alpha ){ @content; }
+ }
+ @else if $point == "beta" {
+ @media screen and (min-width: $breakpoint-beta) { @content; }
+ }
+ @else if $point == "gamma" {
+ @media screen and (min-width: $breakpoint-gamma) { @content; }
+ }
+ @else if $point == "delta" {
+ @media screen and (min-width: $breakpoint-delta) { @content; }
+ }
+ @else if $point == "epsilon" {
+ @media screen and (min-width: $breakpoint-epsilon) { @content; }
+ }
+}
diff --git a/design/sass/screenv1.scss b/design/sass/screenv1.scss
new file mode 100644
index 0000000..f1a1f72
--- /dev/null
+++ b/design/sass/screenv1.scss
@@ -0,0 +1,7 @@
+@import "_fonts.scss";
+@import "_mixins.scss";
+@import "_queries.scss";
+@import "_global.scss";
+@import "_header.scss";
+@import "_content.scss";
+@import "_footer.scss";
diff --git a/design/templates/admin/buttons.html b/design/templates/admin/buttons.html
new file mode 100644
index 0000000..d9d3171
--- /dev/null
+++ b/design/templates/admin/buttons.html
@@ -0,0 +1,52 @@
+<style>
+ .cust {
+ margin-top: 8px;
+ margin-bottom: 16px;
+ }
+ .item {
+ display: block;
+ float: none !important;
+ margin-left: 8px;
+ margin-top: 6px;
+ color: #444;
+ overflow: visible !important;
+ }
+ .item a {
+ text-transform: uppercase;
+ min-width: 180px;
+ font-size: 90%;
+ position: relative;
+ display: inline-block;
+ margin: 0px;
+ padding: 7px 5px;
+ height: 32px;
+ -moz-box-sizing: border-box;
+ cursor: pointer;
+ overflow: hidden;
+ vertical-align: top;
+ color: rgb(255, 255, 255);
+ background: #79aec8;
+ }
+</style>
+
+ <div class="module" id="recent-actions-module">
+ <div class="grp-module" id="grp-recent-actions-module">
+ <h2>Publish Site</h2>
+ {% if message %}
+ <ul class="messagelist">
+ <li style="font-weight: bold; color: red;">{{message}}...</li>
+ </ul>
+ {%endif%}
+ <div class="grp-module">
+ <ul class="grp-listing-small cust">
+ <li class="item"><a href="/admin/build/build?id=builddetails">Build Writing Details</a></li>
+ <li class="item"><a href="/admin/build/build?id=writingarchives">Build Writing Archives</a></li>
+ <li class="item"><a href="/admin/build/build?id=homepage">Build Homepage</a></li>
+ <li class="item"><a href="/admin/build/build?id=sitemap">Build Sitemap</a></li>
+ <li class="item"><a href="/admin/build/build?id=buildrss">Build RSS</a></li>
+ <li class="item"><a href="/admin/build/build?id=pages">Build All Pages</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+
diff --git a/design/templates/admin/index.html b/design/templates/admin/index.html
new file mode 100644
index 0000000..fc857f2
--- /dev/null
+++ b/design/templates/admin/index.html
@@ -0,0 +1,148 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_static %}
+
+{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/dashboard.css" %}" />
+<style>
+.module caption {
+ padding: 5px;
+}
+td, th {
+ font-size: 12px;
+ padding: 5px 8px;
+}
+.module {
+ margin-bottom: 20px;
+}
+</style>
+{% endblock %}
+
+{% block coltype %}colMS{% endblock %}
+
+{% block bodyclass %}dashboard{% endblock %}
+
+{% block breadcrumbs %}{% endblock %}
+
+{% block content %}
+<div id="content-main">
+
+{% if app_list %}
+ <div class="module">
+ <table>
+ <caption><a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">Frequently Used</a></caption>
+ <tr>
+ <th scope="row"><a href="/admin/django_comments/comment/">moderate comments</a></th>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/jrnl/entry/">jrnl</a></th>
+ <td><a href="/admin/jrnl/entry/add/" class="addlink">{% trans 'Add' %}</a></td></tr>
+ <tr>
+ <th scope="row"><a href="/admin/photos/luximage/">photos</a></th>
+ <td><a href="/admin/photos/luximage/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/sightings/sighting/">sightings</a></th>
+ <td><a href="/admin/sightings/sighting/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/fieldnotes/fieldnote/">fieldnotes</a></th>
+ <td><a href="/admin/fieldnotes/fieldnote/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/locations/checkin/">check ins</a></th>
+ <td><a href="/admin/locations/checkin/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/locations/location/">locations</a></th>
+ <td><a href="/admin/locations/location/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/sightings/ap/">dialogue</a></th>
+ <td><a href="/admin/sightings/ap/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/locations/campsite/">campsite</a></th>
+ <td><a href="/admin/locations/campsite/add/" class="addlink">Add</a></td>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/jrnl/home/1/change/">homepage</a></th>
+ </tr>
+ <tr>
+ <th scope="row"><a href="/admin/pages/page/">pages</a></th>
+ <td><a href="/admin/pages/page/add/" class="addlink">Add</a></td>
+ </tr>
+ </table>
+ </div>
+ {% for app in app_list %}
+ <div class="module">
+ <table>
+ <caption>
+ <a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">
+ {% blocktrans with name=app.name %}{{ name }}{% endblocktrans %}
+ </a>
+ </caption>
+ {% for model in app.models %}
+ <tr>
+ {% if model.admin_url %}
+ <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
+ {% else %}
+ <th scope="row">{{ model.name }}</th>
+ {% endif %}
+
+ {% if model.add_url %}
+ <td><a href="{{ model.add_url }}" class="addlink">{% trans 'Add' %}</a></td>
+ {% else %}
+ <td>&nbsp;</td>
+ {% endif %}
+
+ {% if model.admin_url %}
+ <td><a href="{{ model.admin_url }}" class="changelink">{% trans 'Change' %}</a></td>
+ {% else %}
+ <td>&nbsp;</td>
+ {% endif %}
+ </tr>
+ {% if model.name == "Gigs"%}
+ <tr> <th scope="row"><a href="{{ model.admin_url }}monthly/">Income for month</a></th></tr>
+ {% endif %}
+ {% endfor %}
+ </table>
+ </div>
+ {% endfor %}
+{% else %}
+ <p>{% trans "You don't have permission to edit anything." %}</p>
+{% endif %}
+</div>
+{% endblock %}
+
+{% block sidebar %}
+
+<div id="content-related">
+{% include 'admin/buttons.html' %}
+ <div class="module" id="recent-actions-module">
+ <h2>{% trans 'Recent Actions' %}</h2>
+ <h3>{% trans 'My Actions' %}</h3>
+ {% load log %}
+ {% get_admin_log 10 as admin_log for_user user %}
+ {% if not admin_log %}
+ <p>{% trans 'None available' %}</p>
+ {% else %}
+ <ul class="actionlist">
+ {% for entry in admin_log %}
+ <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">
+ {% if entry.is_deletion or not entry.get_admin_url %}
+ {{ entry.object_repr }}
+ {% else %}
+ <a href="{{ entry.get_admin_url }}">{{ entry.object_repr }}</a>
+ {% endif %}
+ <br/>
+ {% if entry.content_type %}
+ <span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span>
+ {% else %}
+ <span class="mini quiet">{% trans 'Unknown content' %}</span>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+ </div>
+</div>
+{% endblock %}
diff --git a/design/templates/admin/message.html b/design/templates/admin/message.html
new file mode 100644
index 0000000..348b15d
--- /dev/null
+++ b/design/templates/admin/message.html
@@ -0,0 +1,20 @@
+{% extends 'admin/base_site.html' %}
+
+{% block content %}
+{{message}}... <span style="color:red;" id="done"></span>
+{% endblock %}
+
+{% block extrahead %}
+<script type="text/javascript">
+function delayer(){
+ console.log("function fired")
+ document.getElementById('done').innerHTML = 'done'
+ window.location = "/admin/"
+}
+
+document.addEventListener("DOMContentLoaded", function(event) {
+ window.setTimeout(delayer, 1000);
+});
+</script>
+{% endblock %}
+
diff --git a/design/templates/base.html b/design/templates/base.html
new file mode 100644
index 0000000..41a8033
--- /dev/null
+++ b/design/templates/base.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html {%block htmlclass%}{%endblock%} dir="ltr" lang="en-US">
+ {% block sitename %}
+<head>
+ <title>{% block pagetitle %}{% endblock %}</title>{%endblock%}
+ <meta charset="utf-8">
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description"
+ content="{% block metadescription %}Scott Gilbertson is a Technical Writer, Copywriter and Editor helping your company increase its online content to boost your traffic and attract new leads. {% endblock %}">
+ <meta name="author" content="Scott Gilbertson">
+ <link rel="alternate"
+ type="application/rss+xml"
+ title="RSS feed"
+ href="/rss/">
+ {%block stylesheet%}<link rel="stylesheet"
+ href="/media/screenv1.css?{% now "u" %}"
+ media="screen">{%endblock%}
+ <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
+ <!--<link rel="manifest" href="/manifest.webmanifest" /> -->
+ {%block extrahead%}{%endblock%}
+</head>
+<body {%block bodyid%}{%endblock%}{%block bodyevents%}{%endblock%}>
+ <div class="wrapper" id="wrapper">
+ <div class="header-wrapper">
+ <header class="site-banner">
+ <div id="logo">
+ <a href="/" title="Home">Libregraf</a>
+ <span class="tagline">Let's Tell Your Story</span>
+ </div>
+ <nav>
+ <ul>
+ <li><a href="/hire-me" title="">Hire Me</a></li>
+ <li><a href="/about" title="">About Scott</a></li>
+ <!--<li><a href="/portfolio" title="">Portfolio</a></li>
+ <li><a href="/praise" title="">Praise</a></li>
+ <li><a href="/contact" title="">Contact</a></li>
+ <li><a href="/blog/" title="">Blog</a></li>-->
+ </ul>
+ </nav>
+ </header>
+ </div>
+ {% block content %}{% endblock %}
+ {% block extrabody %}{% endblock %}
+ <footer class="bl">
+ <p id="license">
+ &copy; {% now "Y" %}
+ <span class="h-card"><a class="p-name u-url" href="https://luxagraf.net/">Scott Gilbertson</a><data class="p-nickname" value="luxagraf"></data><data class="p-locality" value="Athens"></data><data class="p-region" value="Georgia"></data><data class="p-country-name" value="United States"></data></span>.
+ </p>
+ <ul class="footer-nav">
+ <li><a href="/contact" title="contact luxagraf">Contact</a></li>
+ </ul>
+ </footer>
+ </div>
+ {% block js %}{% endblock%}
+<script>
+// Register our service-worker
+if (navigator.serviceWorker) {
+ window.addEventListener('load', function() {
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage({'command': 'trimCaches'});
+ } else {
+ navigator.serviceWorker.register('/media/js/serviceworker.js', {
+ scope: '/'
+ });
+ }
+ });
+}
+</script>
+ {%comment%}
+<!-- Piwik -->
+<script type="text/javascript">
+var _paq = _paq || [];
+_paq.push(["disableCookies"]);
+_paq.push(['trackPageView']);
+_paq.push(['enableLinkTracking']);
+(function() {
+ var u="//stats.luxagraf.net/";
+ _paq.push(['setTrackerUrl', u+'piwik.php']);
+ _paq.push(['setSiteId', 1]);
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
+})();
+</script>
+<noscript><p><img src="//stats.luxagraf.net/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
+<!-- End Piwik Code -->
+{%endcomment%}
+
+</body>
+</html>
+
diff --git a/design/templates/homepage.html b/design/templates/homepage.html
new file mode 100644
index 0000000..ed47bdb
--- /dev/null
+++ b/design/templates/homepage.html
@@ -0,0 +1,88 @@
+{% extends 'base.html' %}
+{% load typogrify_tags %}
+{% block sitename %}
+<head itemscope itemtype="http://schema.org/WebSite">
+ <title itemprop='name'>Everyone has a story - I'll help you tell yours</title>
+ <link rel="canonical" href="https://libregraf.net/" itemprop="url" />
+
+{% endblock %}
+
+{%block bodyid%}id="home" class="home"{%endblock%}
+
+{% block content %}
+<div class="content-wrapper hero-wrapper">
+ <div class="content hero">
+ <div class="hero-pic">
+ <img src="/media/bio.jpg" class="circle-pic" alt="Scott Gilbertson" />
+ </div>
+ <div class="hero-text">
+ <h1 class="hed-alpha">Need Content That Converts?</h1>
+ <h2 class="hed-beta">Let's find the weak spots in your content and strengthen them.</h2>
+ <h2 class="hed-beta">I'll create the content you need to amp up your traffic, leads and&nbsp;sales.</h2>
+ <h3 class="hed-alpha">My writing attracts, engages and converts.</h3>
+ <p>Whether you’re an entrepreneur, small business, or large company, my custom tailored content can catapult your online visibility and give you the traffic boost you need to attract new leads.</p>
+ <p><a href="/hire-me" class="btn btn-hollow">Let's Get Started</a></p>
+ </div>
+ </div>
+</div>
+<div class="content-wrapper">
+ <div class="content">
+ <h4 class="hed-gamma hed-border">How Can I Help You?</h4>
+ <p class="narrow">Since the dawn of the Internet I've been helping people discover the power of stories. Stories pull customers in, stories drive traffic. You don't need &#8220;marketing,&#8221; you need good stories. Here's a few ways I can help tell your story.</p>
+ <div class="home-card-wrapper">
+ <div class="flex">
+ <div class="card card-tiny">
+ <div class="card-image">
+ <img src="/media/img/typing.jpg" alt="hands typing code on laptop" />
+ </div>
+ <h5 class="hed-delta">Technical Writing</h5>
+ <p>My specialty is making the technical easy to understand. I've used my expertise as a developer to write documentation, tutorials, and white papers. I&#8217;ve helped companies like Opera Software, Postmarkapp, Sifter tell their story to developers and users. Lets tell your story.</p>
+ </div>
+ <div class="card card-tiny">
+ <div class="card-image">
+ <img src="/media/img/content.jpg" alt="hands typing code on laptop" />
+ </div>
+ <h5 class="hed-delta">Content Marketing</h5>
+ <p>Content marketing is marketing-speak for telling stories that captivate your audience. Whether you already have a blog or are starting from scratch, I can help refine your strategies and create content focused on achieving measurable results.</p>
+ </div>
+ <div class="card card-tiny">
+ <div class="card-image">
+ <img src="/media/img/pen.jpg" alt="pen editing papers" />
+ </div>
+ <h5 class="hed-delta">Copywriting &amp; Copy editing</h5>
+ <p>Maybe you've already written your story, but could use a hand polishing it up. Whether you need some need some fine tuning of what you already have, or need some help organizing and streamlining your story, I can help.</p>
+ </div>
+ </div>
+ <a href="/hire-me" class="btn btn-hollow btn-link">Hire Me!</a>
+ <a href="https://google.com/" class="btn btn-hollow btn-link">Go Cheap</a>
+ </div>
+ </div>
+</div>
+<div class="content-wrapper">
+ <h6 class="hed-gamma hed-border">Companies I've helped</h6>
+ <div class="border-wrapper">
+ <ul class="list-inline home-logos">
+ <li><a href="https://wired.com/" title="Wired.com"><img src="/media/img/wired.png" alt="Wired logo" /></a></li>
+ <li><a href="https://opera.com/" title="Opera.com"><img src="/media/img/opera.png" alt="Opera Web Browser logo" /></a></li>
+ <li><a href="https://arstechnica.com/" title="ArsTechnica.com"><img src="/media/img/ars.png" alt="Ars logo" /></a></li>
+ <li><a href="https://sifterapp.com/" title="Sifter.com"><img src="/media/img/sifter.png" alt="Sifter logo" /></a></li>
+ <li><a href="https://www.boostmobile.com/" title="boostmobile"><img src="/media/img/boost.png" alt="Boost Mobile logo" /></a></li>
+ <li><a href="https://webmonkey.com/" title="Webmonkey.com"><img src="/media/img/webmonkey.jpg" alt="Webmonkey logo" /></a></li>
+ <!--<li><a href="" title=""><img src="/media/img/budgettravel.jpg" alt="Budget Travel logo" /></li>-->
+ <li><a href="https://postmarkapp.com/" title="Postmark"><img src="/media/img/postmark.png" alt="Postmarkapp logo" /></a></li>
+ <li><a href="https://theregister.co.uk/" title="The register"><img src="/media/img/register.png" alt="Register logo" /></a></li>
+ </ul>
+ </div>
+</div>
+
+<div class="content-wrapper">
+ <div class="content">
+ <div class="home-border-wrapper">
+ <h6 class="hed-gamma hed-border">What Clients Say</h6>
+ </div>
+ </div>
+</div>
+{% endblock %}
+
+#### Copywriting &amp; Copy editing
+Whether you need some writing done from scratch or just need some fine tuning of what you already have, I can help.
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..eb3ebbe
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+import os
+import sys
+d = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(d+"/app")
+sys.path.append(d+"/app/lib")
+sys.path.append(d+"/config")
+if __name__ == '__main__':
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)