diff --git a/aircox_web/assets/styles.scss b/aircox_web/assets/styles.scss
new file mode 100644
index 0000000..b3a8e42
--- /dev/null
+++ b/aircox_web/assets/styles.scss
@@ -0,0 +1,24 @@
+@charset "utf-8";
+@import "~bulma/sass/utilities/_all.sass";
+
+$body-background-color: $light;
+
+@import "~bulma/bulma";
+
+.navbar {
+ margin-bottom: 1em;
+}
+
+.navbar.has-shadow {
+ box-shadow: 0em 0.1em 0.5em rgba(0,0,0,0.1);
+}
+
+.navbar-brand img {
+ min-height: 6em;
+}
+
+.navbar-menu .navbar-item:not(:last-child) {
+ border-right: 1px $grey solid;
+}
+
+
diff --git a/aircox_web/fields.py b/aircox_web/fields.py
new file mode 100644
index 0000000..6212d5e
--- /dev/null
+++ b/aircox_web/fields.py
@@ -0,0 +1,32 @@
+from django.db import models
+
+
+class BaseMinMaxField:
+ def __init__(self, verbose_name=None, name=None, min=None, max=None,
+ **kwargs):
+ super().__init__(verbose_name, name, **kwargs)
+ self.min_value = min
+ self.max_value = max
+
+ def minmax(self, value):
+ return min(self.max_value, max(self.min_value, value))
+
+ def to_python(self, value):
+ return self.minmax(super().to_python(value))
+
+ def get_prep_value(self, value):
+ return super().get_prep_value(self.minmax(value))
+
+
+class MinMaxField(BaseMinMaxField, models.IntegerField):
+ pass
+
+class SmallMinMaxField(BaseMinMaxField, models.SmallIntegerField):
+ pass
+
+class PositiveMinMaxField(BaseMinMaxField, models.PositiveIntegerField):
+ pass
+
+class PositiveSmallMinMaxField(BaseMinMaxField, models.PositiveSmallIntegerField):
+ pass
+
diff --git a/aircox_web/plugins/__init__.py b/aircox_web/plugins/__init__.py
new file mode 100644
index 0000000..9f6dd21
--- /dev/null
+++ b/aircox_web/plugins/__init__.py
@@ -0,0 +1,51 @@
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.utils.html import escape, format_html, mark_safe
+from django.urls import reverse
+
+from .image import ImageBase, Image
+from .richtext import RichText
+
+
+__all__ = ['ImageBase', 'Image', 'RichText']
+
+
+class Link(models.Model):
+ url = models.CharField(
+ _('url'), max_length=128, null=True, blank=True,
+ )
+ page = models.ForeignKey(
+ 'Page', models.SET_NULL, null=True, blank=True,
+ verbose_name=_('Link to a page')
+ )
+ text = models.CharField(_('text'), max_length=64, null=True, blank=True)
+ info = models.CharField(_('info'), max_length=128, null=True, blank=True,
+ help_text=_('link description displayed as tooltip'))
+ blank = models.BooleanField(_('new window'), default=False,
+ help_text=_('open in a new window'))
+ css_class=""
+
+ def get_url(self):
+ if self.page:
+ return self.page.path #reverse('page', args=[self.page.path])
+ return self.url or ''
+
+ def render(self):
+ # FIXME: quote
+ return format_html(
+ '{}',
+ self.get_url(), escape(self.info),
+ ' class=' + escape(self.css_class) + ''
+ if self.css_class else '',
+ self.text or (self.page and self.page.title) or '',
+ )
+
+ class Meta:
+ abstract = True
+
+
+class Search(models.Model):
+ class Meta:
+ abstract = True
+
+
diff --git a/aircox_web/plugins/image.py b/aircox_web/plugins/image.py
new file mode 100644
index 0000000..51b49b4
--- /dev/null
+++ b/aircox_web/plugins/image.py
@@ -0,0 +1,47 @@
+from django.db import models
+from django.templatetags.static import static
+from django.utils.translation import ugettext_lazy as _
+from django.utils.html import format_html, mark_safe
+
+from easy_thumbnails.files import get_thumbnailer
+from filer.fields.image import FilerImageField
+
+__all__ = ['ImageBase', 'Image']
+
+
+class ImageBase(models.Model):
+ image = FilerImageField(
+ on_delete=models.CASCADE,
+ verbose_name=_('image'),
+ )
+ width = None
+ height = None
+ crop = False
+
+ class Meta:
+ abstract = True
+
+ @property
+ def thumbnail(self):
+ if self.width == None and self.height == None:
+ return self.image
+ opts = {}
+ if self.crop:
+ opts['crop'] = 'smart'
+ opts['size'] = (self.width or 0, self.height or 0)
+ thumbnailer = get_thumbnailer(self.image)
+ return thumbnailer.get_thumbnail(opts)
+
+ def render(self):
+ return format_html('
', self.thumbnail.url)
+
+
+class Image(ImageBase):
+ width = models.PositiveSmallIntegerField(blank=True,null=True)
+ height = models.PositiveSmallIntegerField(blank=True,null=True)
+ crop = models.BooleanField(default=False)
+
+ class Meta:
+ abstract = True
+
+
diff --git a/aircox_web/plugins/timetable.py b/aircox_web/plugins/timetable.py
new file mode 100644
index 0000000..8875b33
--- /dev/null
+++ b/aircox_web/plugins/timetable.py
@@ -0,0 +1,37 @@
+import datetime
+
+from django.db import models
+from django.templatetags.static import static
+from django.utils.translation import ugettext_lazy as _
+
+from aircox import models as aircox
+from aircox_web.fields import PositiveSmallMinMaxField
+
+
+class Timetable(models.Model):
+ station = models.ForeignKey(
+ aircox.Station, models.CASCADE, verbose_name=_('station'),
+ )
+ days_before = models.PositiveSmallMinMaxField(
+ _('days before'), min=0, max=6,
+ help_text=_('Count of days displayed current date'),
+ )
+ days_after = models.PositiveSmallMinMaxField(
+ _('days after'), min=0, max=6,
+ help_text=_('Count of days displayed current date'),
+ )
+
+ def get_queryset(self, date=None):
+ date = date if date is not None else datetime.date.today()
+ qs = aircox.Diffusion.objects.station(self.station)
+ if self.days_before is None and self.days_after is None:
+ return qs.at(date)
+
+ start = date - datetime.timedelta(days=self.days_before) \
+ if self.days_before else date
+ stop = date + datetime.timedelta(days=self.days_after) \
+ if self.days_after else date
+ return aircox.Diffusion.objects.station(self.station) \
+ .after(start).before(stop)
+
+