]> git.ipfire.org Git - ipfire.org.git/commitdiff
Merge remote-tracking branch 'origin/new-design' into new-design
authorRico Hoppe <rico.hoppe@ipfire.org>
Tue, 28 Nov 2023 15:16:16 +0000 (15:16 +0000)
committerRico Hoppe <rico.hoppe@ipfire.org>
Tue, 28 Nov 2023 15:16:16 +0000 (15:16 +0000)
98 files changed:
.gitignore
Makefile.am
configure.ac
m4/.gitignore [new file with mode: 0644]
m4/ax_python_module.m4 [new file with mode: 0644]
migrate.sql [new file with mode: 0644]
requirements.txt [deleted file]
src/backend/accounts.py
src/backend/base.py
src/backend/cache.py [new file with mode: 0644]
src/backend/database.py
src/backend/fireinfo.py
src/backend/memcached.py [deleted file]
src/backend/messages.py
src/backend/misc.py
src/backend/ratelimit.py
src/backend/releases.py
src/backend/wiki.py
src/backend/zeiterfassung.py
src/sass/_fonts.sass
src/sass/listing.sass
src/sass/main.sass
src/scripts/ipfire.org.in
src/static/img/auth/register.jpg [new file with mode: 0644]
src/templates/auth/login.html
src/templates/auth/messages/donation-reminder.html
src/templates/auth/messages/password-reset.html
src/templates/auth/messages/profile-setup-2.html
src/templates/auth/messages/profile-setup.html
src/templates/auth/messages/register.html
src/templates/auth/password-reset-initiation.html
src/templates/auth/password-reset-successful.html
src/templates/auth/password-reset.html
src/templates/auth/register-success.html
src/templates/auth/register.html
src/templates/base.html
src/templates/blog/drafts.html
src/templates/blog/index.html
src/templates/blog/messages/announcement.html
src/templates/blog/modules/history-navigation.html
src/templates/blog/modules/list.html
src/templates/blog/post.html
src/templates/blog/write.html
src/templates/blog/year.html
src/templates/docs/modules/header.html
src/templates/docs/recent-changes.html
src/templates/docs/search-results.html
src/templates/donate/donate.html
src/templates/donate/messages/christmas-1.html
src/templates/donate/messages/christmas-1.txt
src/templates/donate/messages/christmas-2.html
src/templates/donate/messages/christmas-2.txt
src/templates/donate/messages/christmas-3.html
src/templates/donate/messages/christmas-3.txt
src/templates/donate/messages/christmas-4.html
src/templates/donate/messages/christmas-4.txt
src/templates/donate/thank-you.html
src/templates/downloads/mirrors.html
src/templates/downloads/release.html
src/templates/downloads/thank-you.html
src/templates/fireinfo/admin.html
src/templates/fireinfo/driver.html
src/templates/fireinfo/index.html
src/templates/fireinfo/processors.html
src/templates/fireinfo/profile.html
src/templates/fireinfo/releases.html
src/templates/fireinfo/vendors.html
src/templates/index.html
src/templates/location/download.html
src/templates/location/how-to-use.html
src/templates/location/index.html
src/templates/location/lookup.html
src/templates/messages/base-promo.html
src/templates/messages/base.html
src/templates/messages/fonts.sass [new file with mode: 0644]
src/templates/messages/main.sass
src/templates/modules/ipfire-logo.html [new file with mode: 0644]
src/templates/nopaste/create.html
src/templates/static/about.html
src/templates/static/help.html
src/templates/static/legal.html
src/templates/static/sitemap.html
src/templates/users/delete.html
src/templates/users/edit.html
src/templates/users/groups/index.html
src/templates/users/groups/show.html
src/templates/users/index.html
src/templates/users/show.html
src/templates/voip/index.html
src/web/__init__.py
src/web/base.py
src/web/blog.py
src/web/docs.py
src/web/donate.py
src/web/fireinfo.py
src/web/iuse.py
src/web/ui_modules.py
src/web/users.py

index 9a3d46440444990aa108409c64bf916426f5ae35..edf2f9e3849a343e9e493234bf8849c867684f09 100644 (file)
 /src/static/favicon.ico
 /src/static/img/apple-touch-icon-*-precomposed.png
 /src/systemd/ipfire.org-webapp-*.service
-/src/templates/messages/main.css
+/src/templates/messages/*.css
 /ipfire.org.conf.sample
 .DS_Store
 Makefile
 Makefile.in
 stamp-*
+*@*.jpg
 *.bak
 *.py[co]
 *.tar.gz
index ea3350be29aa34ac7ab0c9d67155218c818b9be0..1ed1a4983f29af37f403159e80cd2f567c43f353 100644 (file)
@@ -53,6 +53,7 @@ backend_PYTHON = \
        src/backend/base.py \
        src/backend/blog.py \
        src/backend/bugzilla.py \
+       src/backend/cache.py \
        src/backend/campaigns.py \
        src/backend/countries.py \
        src/backend/database.py \
@@ -61,7 +62,6 @@ backend_PYTHON = \
        src/backend/httpclient.py \
        src/backend/hwdata.py \
        src/backend/iuse.py \
-       src/backend/memcached.py \
        src/backend/messages.py \
        src/backend/mirrors.py \
        src/backend/misc.py \
@@ -259,12 +259,14 @@ templates_locationdir = $(templatesdir)/location
 templates_messages_DATA = \
        src/templates/messages/base.html \
        src/templates/messages/base-promo.html \
+       src/templates/messages/fonts.css \
        src/templates/messages/main.css
 
 templates_messagesdir = $(templatesdir)/messages
 
 templates_modules_DATA = \
        src/templates/modules/christmas-banner.html \
+       src/templates/modules/ipfire-logo.html \
        src/templates/modules/map.html \
        src/templates/modules/progress-bar.html
 
@@ -358,9 +360,11 @@ SASS_FILES = \
 
 EXTRA_DIST += \
        src/sass/listing.sass \
+       src/templates/messages/fonts.sass \
        src/templates/messages/main.sass
 
 CLEANFILES += \
+       src/templates/messages/fonts.css \
        src/templates/messages/main.css
 
 static_DATA = \
@@ -963,6 +967,19 @@ static_img_DATA = \
 
 static_imgdir = $(staticdir)/img
 
+# From https://www.pexels.com/photo/123-let-s-go-imaginary-text-704767/
+
+dist_static_img_auth_DATA = \
+       src/static/img/auth/register.jpg
+
+static_img_auth_DATA = \
+       src/static/img/auth/register@600.jpg
+
+CLEANFILES += \
+       src/static/img/auth/register@600.jpg
+
+static_img_authdir = $(static_imgdir)/auth
+
 static_images_tux_DATA = \
        src/static/img/tux/ipfire_tux_16x16.png \
        src/static/img/tux/ipfire_tux_20x20.png \
@@ -1086,6 +1103,11 @@ src/static/img/apple-touch-icon-%-precomposed.png: src/static/img/ipfire-tux.png
                -extent $(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@)x$(patsubst src/static/img/apple-touch-icon-%-precomposed.png,%,$@) \
                $< $@
 
+# Resizes images for being used in messages which are 600px wide
+%@600.jpg: %.jpg
+       $(AM_V_GEN)$(MKDIR_P) $(dir $@) && \
+       $(CONVERT) -units PixelsPerInch $< -resize 600x -strip -quality 85 $@
+
 # Video Stuff
 
 FFMPEG += \
index c5f4c7f2d4af37f79c20c129983f8d716a40c90b..ae6a9af11894bbe25eb705674d755abf4b842b50 100644 (file)
@@ -6,6 +6,7 @@ AC_INIT([ipfire.org],
        [ipfire.org],
        [https://www.ipfire.org/])
 
+AC_CONFIG_MACRO_DIR([m4])
 AC_CONFIG_AUX_DIR([build-aux])
 
 AC_PREFIX_DEFAULT([/usr])
@@ -26,7 +27,23 @@ AC_PROG_MKDIR_P
 AC_PROG_SED
 
 # Python
-AM_PATH_PYTHON([3.4])
+AM_PATH_PYTHON([3.11])
+
+AX_PYTHON_MODULE([PIL], [fatal])
+AX_PYTHON_MODULE([feedparser], [fatal])
+AX_PYTHON_MODULE([html2text], [fatal])
+AX_PYTHON_MODULE([iso3166], [fatal])
+AX_PYTHON_MODULE([jsonschema], [fatal])
+AX_PYTHON_MODULE([kerberos], [fatal])
+AX_PYTHON_MODULE([ldap], [fatal])
+AX_PYTHON_MODULE([panoramisk], [fatal])
+AX_PYTHON_MODULE([phonenumbers], [fatal])
+AX_PYTHON_MODULE([psycopg], [fatal])
+AX_PYTHON_MODULE([pycares], [fatal])
+AX_PYTHON_MODULE([pynliner], [fatal])
+AX_PYTHON_MODULE([redis.asyncio], [fatal])
+AX_PYTHON_MODULE([tornado], [fatal])
+AX_PYTHON_MODULE([zxcvbn], [fatal])
 
 # sass
 AC_CHECK_PROG(SASSC, [sassc], [sassc])
diff --git a/m4/.gitignore b/m4/.gitignore
new file mode 100644 (file)
index 0000000..55eaa80
--- /dev/null
@@ -0,0 +1,6 @@
+intltool.m4
+libtool.m4
+ltoptions.m4
+ltsugar.m4
+ltversion.m4
+lt~obsolete.m4
diff --git a/m4/ax_python_module.m4 b/m4/ax_python_module.m4
new file mode 100644 (file)
index 0000000..f0f873d
--- /dev/null
@@ -0,0 +1,56 @@
+# ===========================================================================
+#     https://www.gnu.org/software/autoconf-archive/ax_python_module.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+#   AX_PYTHON_MODULE(modname[, fatal, python])
+#
+# DESCRIPTION
+#
+#   Checks for Python module.
+#
+#   If fatal is non-empty then absence of a module will trigger an error.
+#   The third parameter can either be "python" for Python 2 or "python3" for
+#   Python 3; defaults to Python 3.
+#
+# LICENSE
+#
+#   Copyright (c) 2008 Andrew Collier
+#
+#   Copying and distribution of this file, with or without modification, are
+#   permitted in any medium without royalty provided the copyright notice
+#   and this notice are preserved. This file is offered as-is, without any
+#   warranty.
+
+#serial 9
+
+AU_ALIAS([AC_PYTHON_MODULE], [AX_PYTHON_MODULE])
+AC_DEFUN([AX_PYTHON_MODULE],[
+    if test -z $PYTHON;
+    then
+        if test -z "$3";
+        then
+            PYTHON="python3"
+        else
+            PYTHON="$3"
+        fi
+    fi
+    PYTHON_NAME=`basename $PYTHON`
+    AC_MSG_CHECKING($PYTHON_NAME module: $1)
+    $PYTHON -c "import $1" 2>/dev/null
+    if test $? -eq 0;
+    then
+        AC_MSG_RESULT(yes)
+        eval AS_TR_CPP(HAVE_PYMOD_$1)=yes
+    else
+        AC_MSG_RESULT(no)
+        eval AS_TR_CPP(HAVE_PYMOD_$1)=no
+        #
+        if test -n "$2"
+        then
+            AC_MSG_ERROR(failed to find required module $1)
+            exit 1
+        fi
+    fi
+])
diff --git a/migrate.sql b/migrate.sql
new file mode 100644 (file)
index 0000000..78a4178
--- /dev/null
@@ -0,0 +1,188 @@
+START TRANSACTION;
+
+-- CREATE INDEX fireinfo_search ON fireinfo USING gin (blob) WHERE expired_at IS NULL;
+-- CREATE UNIQUE INDEX fireinfo_current ON fireinfo USING btree (profile_id) WHERE expired_at IS NULL
+
+-- CREATE INDEX fireinfo_releases_current ON fireinfo USING hash((blob->'system'->'release')) WHERE expired_at IS NULL;
+-- CREATE INDEX fireinfo_releases ON fireinfo USING hash((blob->'system'->'release'));
+
+-- CREATE INDEX fireinfo_arches_current ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL AND expired_at IS NULL;
+-- CREATE INDEX fireinfo_arches ON fireinfo USING hash((blob->'cpu'->'arch')) WHERE blob->'cpu'->'arch' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_cpu_vendors ON fireinfo USING hash((blob->'cpu'->'vendor')) WHERE blob->'cpu'->'vendor' IS NOT NULL;
+
+-- CREATE INDEX fireinfo_hypervisor_vendors_current ON fireinfo USING hash((blob->'hypervisor'->'vendor')) WHERE expired_at IS NULL AND CAST((blob->'system'->'virtual') AS boolean) IS TRUE;
+
+-- XXX virtual index
+
+TRUNCATE TABLE fireinfo;
+
+--EXPLAIN
+
+INSERT INTO fireinfo
+
+SELECT
+       p.public_id AS profile_id,
+       p.time_created AS created_at,
+       (
+               CASE
+                       WHEN p.time_valid <= CURRENT_TIMESTAMP THEN p.time_valid
+                       ELSE NULL
+               END
+       ) AS expired_at,
+       0 AS version,
+       (
+               -- Empty the profile if we don't have any data
+               CASE WHEN profile_arches.arch_id IS NULL THEN NULL
+
+               -- Otherwise do some hard work...
+               ELSE
+                       -- CPU
+                       jsonb_build_object('cpu',
+                               jsonb_build_object(
+                                       'arch',         arches.name,
+                                       'bogomips',     profile_processors.bogomips,
+                                       'speed',        profile_processors.clock_speed,
+
+                                       'vendor',       processors.vendor,
+                                       'model',        processors.model,
+                                       'model_string', processors.model_string,
+                                       'stepping',     processors.stepping,
+                                       'flags',        processors.flags,
+                                       'family',       processors.family,
+                                       'count',        processors.core_count
+                               )
+                       )
+
+                       -- System
+                       || jsonb_build_object('system',
+                               jsonb_build_object(
+                                       'kernel',    kernels.name,
+                                       'language',  profile_languages.language,
+                                       'memory',    profile_memory.amount,
+                                       'release',   releases.name,
+                                       'root_size', profile_storage.amount,
+                                       'vendor',    systems.vendor,
+                                       'model',     systems.model,
+                                       'virtual',   CASE WHEN hypervisors.id IS NULL THEN FALSE ELSE TRUE END
+                               )
+                       )
+
+                       -- Hypervisor
+                       || CASE
+                               WHEN hypervisors.id IS NULL THEN jsonb_build_object()
+                               ELSE
+                                       jsonb_build_object(
+                                               'hypervisor',
+                                               json_build_object('vendor', hypervisors.name)
+                                       )
+                               END
+
+                       -- Devices
+                       || jsonb_build_object('devices', devices.devices)
+
+                       -- Networks
+                       || jsonb_build_object('networks',
+                               jsonb_build_object(
+                                       'green',  profile_networks.has_green,
+                                       'blue',   profile_networks.has_blue,
+                                       'orange', profile_networks.has_orange,
+                                       'red',    profile_networks.has_red
+                               )
+                       )
+               END
+       ) AS blob,
+       p.time_updated AS last_updated_at,
+       p.private_id AS private_id,
+       locations.location AS country_code
+
+FROM fireinfo_profiles p
+
+LEFT JOIN
+       fireinfo_profiles_locations locations ON p.id = locations.profile_id
+
+LEFT JOIN
+       fireinfo_profiles_arches profile_arches ON p.id = profile_arches.profile_id
+
+LEFT JOIN
+       fireinfo_arches arches ON profile_arches.arch_id = arches.id
+
+LEFT JOIN
+       (
+               SELECT
+                       profile_devices.profile_id AS profile_id,
+                       jsonb_agg(
+                               jsonb_build_object(
+                                       'deviceclass', devices.deviceclass,
+                                       'subsystem',   devices.subsystem,
+                                       'vendor',      devices.vendor,
+                                       'model',       devices.model,
+                                       'sub_vendor',  devices.sub_vendor,
+                                       'sub_model',   devices.sub_model,
+                                       'driver',      devices.driver
+                               )
+                       ) AS devices
+               FROM
+                       fireinfo_profiles_devices profile_devices
+               LEFT JOIN
+                       fireinfo_devices devices ON profile_devices.device_id = devices.id
+               GROUP BY
+                       profile_devices.profile_id
+       ) devices ON p.id = devices.profile_id
+
+LEFT JOIN
+       fireinfo_profiles_processors profile_processors ON p.id = profile_processors.profile_id
+
+LEFT JOIN
+       fireinfo_processors processors ON profile_processors.processor_id = processors.id
+
+LEFT JOIN
+       fireinfo_profiles_kernels profile_kernels ON p.id = profile_kernels.profile_id
+
+LEFT JOIN
+       fireinfo_kernels kernels ON profile_kernels.kernel_id = kernels.id
+
+LEFT JOIN
+       fireinfo_profiles_languages profile_languages ON p.id = profile_languages.profile_id
+
+LEFT JOIN
+       fireinfo_profiles_memory profile_memory ON p.id = profile_memory.profile_id
+
+LEFT JOIN
+       fireinfo_profiles_releases profile_releases ON p.id = profile_releases.profile_id
+
+LEFT JOIN
+       fireinfo_releases releases ON profile_releases.release_id = releases.id
+
+LEFT JOIN
+       fireinfo_profiles_storage profile_storage ON p.id = profile_storage.profile_id
+
+LEFT JOIN
+       fireinfo_profiles_systems profile_systems ON p.id = profile_systems.profile_id
+
+LEFT JOIN
+       fireinfo_systems systems ON profile_systems.system_id  = systems.id
+
+LEFT JOIN
+       fireinfo_profiles_virtual profile_virtual ON p.id = profile_virtual.profile_id
+
+LEFT JOIN
+       fireinfo_hypervisors hypervisors ON profile_virtual.hypervisor_id = hypervisors.id
+
+LEFT JOIN
+       fireinfo_profiles_networks profile_networks ON p.id = profile_networks.profile_id
+
+--WHERE
+-- XXX TO FIND A PROFILE WITH DATA
+--     profile_processors.profile_id IS NOT NULL
+
+-- XXX TO FIND A VIRTUAL PROFILE
+--profile_virtual.hypervisor_id IS NOT NULL
+
+--ORDER BY
+--     time_created DESC
+
+--LIMIT 1
+;
+
+COMMIT;
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644 (file)
index 7fde1ae..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-asn1crypto==0.24.0
-backports-abc==0.5
-certifi==2019.3.9
-cffi==1.11.5
-chardet==3.0.4
-cryptography==2.3.1
-ecdsa==0.13
-feedparser==5.2.1
-file-magic==0.4.0
-html5lib==1.0.1
-idna==2.7
-iso3166==0.9
-ldap3==2.5.1
-Markdown==3.1.1
-oauthlib==3.0.1
-phonenumbers==8.9.15
-Pillow==5.3.0
-psycopg2-binary==2.7.5
-py-dateutil==2.2
-pyasn1==0.4.4
-pyasn1-modules==0.2.2
-pycares==2.3.0
-pycparser==2.19
-pycrypto==2.6.1
-pycurl==7.43.0
-Pygments==2.4.2
-python-ldap==3.1.0
-python3-memcached==1.51
-requests==2.21.0
-requests-oauthlib==1.2.0
-sgmllib3k==1.0.0
-six==1.11.0
-textile==3.0.3
-tornado==6.0.2
-twython==3.7.0
-urllib3==1.24.3
-webencodings==0.5.1
-zxcvbn==4.4.27
index b6a3e1b3d8ae0bfe437e66a875b954e866efd0a6..a9942085fcbb1a18d0df619284b339df8f3b824b 100644 (file)
@@ -178,14 +178,7 @@ class Accounts(Object):
                self.search_base = self.settings.get("ldap_search_base")
 
        def __len__(self):
-               count = self.memcache.get("accounts:count")
-
-               if count is None:
-                       count = self._count("(objectClass=person)")
-
-                       self.memcache.set("accounts:count", count, 300)
-
-               return count
+               return self._count("(objectClass=person)")
 
        def __iter__(self):
                accounts = self._search("(objectClass=person)")
@@ -1227,17 +1220,23 @@ class Account(LDAPObject):
 
        # Avatar
 
-       def has_avatar(self):
-               has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
-               if has_avatar is None:
-                       has_avatar = True if self.get_avatar() else False
+       @property
+       def avatar_hash(self):
+               payload = (
+                       self.uid,
+                       "%s" % self.modified_at,
+               )
+
+               # String the payload together
+               payload = "-".join(payload)
 
-                       # Cache avatar status for up to 24 hours
-                       self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
+               # Run MD5() over the payload
+               h = hashlib.new("md5", payload.encode())
 
-               return has_avatar
+               return h.hexdigest()[:7]
 
        def avatar_url(self, size=None, absolute=False):
+               # This cannot be async because we are calling it from the template engine
                url = "/users/%s.jpg?h=%s" % (self.uid, self.avatar_hash)
 
                # Return an absolute URL
@@ -1249,7 +1248,7 @@ class Account(LDAPObject):
 
                return url
 
-       def get_avatar(self, size=None):
+       async def get_avatar(self, size=None):
                photo = self._get_bytes("jpegPhoto")
 
                # Exit if no avatar is available
@@ -1261,7 +1260,7 @@ class Account(LDAPObject):
                        return photo
 
                # Try to retrieve something from the cache
-               avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
+               avatar = await self.backend.cache.get("accounts:%s:avatar:%s" % (self.dn, size))
                if avatar:
                        return avatar
 
@@ -1269,31 +1268,13 @@ class Account(LDAPObject):
                avatar = util.generate_thumbnail(photo, size, square=True)
 
                # Save to cache for 15m
-               self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
+               await self.backend.cache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
 
                return avatar
 
-       @property
-       def avatar_hash(self):
-               hash = self.memcache.get("accounts:%s:avatar-hash" % self.dn)
-               if not hash:
-                       h = hashlib.new("md5")
-                       h.update(self.get_avatar() or b"")
-                       hash = h.hexdigest()[:7]
-
-                       self.memcache.set("accounts:%s:avatar-hash" % self.dn, hash, 86400)
-
-               return hash
-
        def upload_avatar(self, avatar):
                self._set("jpegPhoto", avatar)
 
-               # Delete cached avatar status
-               self.memcache.delete("accounts:%s:has-avatar" % self.dn)
-
-               # Delete avatar hash
-               self.memcache.delete("accounts:%s:avatar-hash" % self.dn)
-
        # Consent to promotional emails
 
        def get_consents_to_promotional_emails(self):
index 40e1c7a5a39933f9c1ddfb576432586bb6c3bf1b..f5d267f4a68277dbb42803820c6823d8477c29ef 100644 (file)
@@ -11,12 +11,12 @@ from . import accounts
 from . import asterisk
 from . import blog
 from . import bugzilla
+from . import cache
 from . import campaigns
 from . import database
 from . import fireinfo
 from . import httpclient
 from . import iuse
-from . import memcached
 from . import messages
 from . import mirrors
 from . import netboot
@@ -56,13 +56,14 @@ class Backend(object):
                # Create HTTPClient
                self.http_client = httpclient.HTTPClient(self)
 
-               # Initialize settings first.
+               # Initialize the cache
+               self.cache = cache.Cache(self)
+
+               # Initialize settings first
                self.settings = settings.Settings(self)
-               self.memcache = memcached.Memcached(self)
 
                # Initialize backend modules.
                self.accounts = accounts.Accounts(self)
-               self.asterisk = asterisk.Asterisk(self)
                self.bugzilla = bugzilla.Bugzilla(self)
                self.fireinfo = fireinfo.Fireinfo(self)
                self.iuse = iuse.IUse(self)
@@ -163,6 +164,10 @@ class Backend(object):
                if r:
                        raise SystemExit(r)
 
+       @lazy_property
+       def asterisk(self):
+               return asterisk.Asterisk(self)
+
        @lazy_property
        def campaigns(self):
                return campaigns.Campaigns(self)
diff --git a/src/backend/cache.py b/src/backend/cache.py
new file mode 100644 (file)
index 0000000..54bb527
--- /dev/null
@@ -0,0 +1,114 @@
+#!/usr/bin/python3
+
+import asyncio
+import logging
+import redis.asyncio
+
+from .decorators import *
+
+# Setup logging
+log = logging.getLogger()
+
+class Cache(object):
+       def __init__(self, backend):
+               self.backend = backend
+
+               # Stores connections assigned to tasks
+               self.__connections = {}
+
+               # Create a connection pool
+               self.pool = redis.asyncio.connection.ConnectionPool.from_url(
+                       "redis://localhost:6379/0",
+               )
+
+       async def connection(self, *args, **kwargs):
+               """
+                       Returns a connection from the pool
+               """
+               # Fetch the current task
+               task = asyncio.current_task()
+
+               assert task, "Could not determine task"
+
+               # Try returning the same connection to the same task
+               try:
+                       return self.__connections[task]
+               except KeyError:
+                       pass
+
+               # Fetch a new connection from the pool
+               conn = await redis.asyncio.Redis(
+                       connection_pool=self.pool,
+                       single_connection_client=True,
+               )
+
+               # Store the connection
+               self.__connections[task] = conn
+
+               log.debug("Assigning cache connection %s to %s" % (conn, task))
+
+               # When the task finishes, release the connection
+               task.add_done_callback(self.__release_connection)
+
+               return conn
+
+       def __release_connection(self, task):
+               loop = asyncio.get_running_loop()
+
+               # Retrieve the connection
+               try:
+                       conn = self.__connections[task]
+               except KeyError:
+                       return
+
+               log.debug("Releasing cache connection %s of %s" % (conn, task))
+
+               # Delete it
+               del self.__connections[task]
+
+               # Return the connection back into the pool
+               asyncio.run_coroutine_threadsafe(conn.close(), loop)
+
+       async def _run(self, command, *args, **kwargs):
+               # Fetch our connection
+               conn = await self.connection()
+
+               # Get the function
+               func = getattr(conn, command)
+
+               # Call the function
+               return await func(*args, **kwargs)
+
+       async def get(self, *args, **kwargs):
+               """
+                       Fetches the value of a cached key
+               """
+               return await self._run("get", *args, **kwargs)
+
+       async def set(self, *args, **kwargs):
+               """
+                       Puts something into the cache
+               """
+               return await self._run("set", *args, **kwargs)
+
+       async def delete(self, *args, **kwargs):
+               """
+                       Deletes the key from the cache
+               """
+               return await self._run("delete", *args, **kwargs)
+
+       async def transaction(self, *args, **kwargs):
+               """
+                       Returns a new transaction
+               """
+               conn = await self.connection()
+
+               return await conn.transaction(*args, **kwargs)
+
+       async def pipeline(self, *args, **kwargs):
+               """
+                       Returns a new pipeline
+               """
+               conn = await self.connection()
+
+               return conn.pipeline(*args, **kwargs)
index bf3cf108c8775dc9638cba47792037802f176246..d45031caba4e432a966dc19c6faf31f60d07b37f 100644 (file)
@@ -18,7 +18,7 @@ import time
 from . import misc
 
 # Setup logging
-log = logging.getLogger("pbs.database")
+log = logging.getLogger()
 
 class Connection(object):
        """
@@ -51,14 +51,14 @@ class Connection(object):
                        configure=self.__configure,
 
                        # Set limits for min/max connections in the pool
-                       min_size=4,
-                       max_size=128,
+                       min_size=8,
+                       max_size=512,
 
                        # Give clients up to one minute to retrieve a connection
                        timeout=60,
 
-                       # Close connections after they have been idle for one minute
-                       max_idle=60,
+                       # Close connections after they have been idle for a few seconds
+                       max_idle=5,
                )
 
        def __configure(self, conn):
index f2273507d45194f5e3dec8a0449e7130a628bc09..260572ae3a592ef4eb981d8a72d753f43a794846 100644 (file)
@@ -2,13 +2,15 @@
 
 import datetime
 import iso3166
+import json
+import jsonschema
 import logging
 import re
 
-from . import database
 from . import hwdata
 from . import util
 from .misc import Object
+from .decorators import *
 
 N_ = lambda x: x
 
@@ -80,31 +82,213 @@ CPU_STRINGS = (
        (r"Feroceon .*", r"ARM Feroceon"),
 )
 
-IGNORED_DEVICES = ["usb",]
-
-class ProfileDict(object):
-       def __init__(self, data):
-               self._data = data
+PROFILE_SCHEMA = {
+       "$schema"     : "https://json-schema.org/draft/2020-12/schema",
+       "$id"         : "https://fireinfo.ipfire.org/profile.schema.json",
+       "title"       : "Fireinfo Profile",
+       "description" : "Fireinfo Profile",
+       "type"        : "object",
 
+       # Properties
+       "properties" : {
+               # Processor
+               "cpu" : {
+                       "type" : "object",
+                       "properties" : {
+                               "arch" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^[a-z0-9\_]{,8}$",
+                               },
+                               "count" : {
+                                       "type" : "integer",
+                               },
+                               "family" : {
+                                       "type" : "integer",
+                               },
+                               "flags" : {
+                                       "type" : "array",
+                                       "items" : {
+                                               "type"    : "string",
+                                               "pattern" : r"^.{,24}$",
+                                       },
+                               },
+                               "model" : {
+                                       "type" : "integer",
+                               },
+                               "model_string" : {
+                                       "type" : "string",
+                                       "pattern" : r"^.{,80}$",
+                               },
+                               "speed" : {
+                                       "type" : "number",
+                               },
+                               "stepping" : {
+                                       "type" : "integer",
+                               },
+                               "vendor" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,80}$",
+                               },
+                       },
+                       "additionalProperties" : False,
+                       "required" : [
+                               "arch",
+                               "count",
+                               "family",
+                               "flags",
+                               "model",
+                               "model_string",
+                               "speed",
+                               "stepping",
+                               "vendor",
+                       ],
+               },
 
-class ProfileNetwork(ProfileDict):
-       def __eq__(self, other):
-               if other is None:
-                       return False
+               # Devices
+               "devices" : {
+                       "type" : "array",
+                       "items" : {
+                               "type" : "object",
+                               "properties" : {
+                                       "deviceclass" : {
+                                               "type"    : ["string", "null"],
+                                               "pattern" : r"^.{,20}$",
+                                       },
+                                       "driver" : {
+                                               "type"    : ["string", "null"],
+                                               "pattern" : r"^.{,24}$",
+                                       },
+                                       "model" : {
+                                               "type"    : "string",
+                                               "pattern" : r"^[a-z0-9]{4}$",
+                                       },
+                                       "sub_model" : {
+                                               "type"    : ["string", "null"],
+                                               "pattern" : r"^[a-z0-9]{4}$",
+                                       },
+                                       "sub_vendor" : {
+                                               "type"    : ["string", "null"],
+                                               "pattern" : r"^[a-z0-9]{4}$",
+                                       },
+                                       "subsystem" : {
+                                               "type"    : "string",
+                                               "pattern" : r"^[a-z]{3}$",
+                                       },
+                                       "vendor" : {
+                                               "type"    : "string",
+                                               "pattern" : r"^[a-z0-9]{4}$",
+                                       },
+                               },
+                               "additionalProperties" : False,
+                               "required" : [
+                                       "deviceclass",
+                                       "driver",
+                                       "model",
+                                       "subsystem",
+                                       "vendor",
+                               ],
+                       },
+               },
 
-               if not self.has_red == other.has_red:
-                       return False
+               # Network
+               "network" : {
+                       "type" : "object",
+                       "properties" : {
+                               "blue" : {
+                                       "type" : "boolean",
+                               },
+                               "green" : {
+                                       "type" : "boolean",
+                               },
+                               "orange" : {
+                                       "type" : "boolean",
+                               },
+                               "red" : {
+                                       "type" : "boolean",
+                               },
+                       },
+                       "additionalProperties" : False,
+               },
 
-               if not self.has_green == other.has_green:
-                       return False
+               # System
+               "system" : {
+                       "type" : "object",
+                       "properties" : {
+                               "kernel_release" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,40}$",
+                               },
+                               "language" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^[a-z]{2}$",
+                               },
+                               "memory" : {
+                                       "type" : "integer",
+                               },
+                               "model" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,80}$",
+                               },
+                               "release" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,80}$",
+                               },
+                               "root_size" : {
+                                       "type" : "number",
+                               },
+                               "vendor" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,80}$",
+                               },
+                               "virtual" : {
+                                       "type" : "boolean"
+                               },
+                       },
+                       "additionalProperties" : False,
+                       "required" : [
+                               "kernel_release",
+                               "language",
+                               "memory",
+                               "model",
+                               "release",
+                               "root_size",
+                               "vendor",
+                               "virtual",
+                       ],
+               },
 
-               if not self.has_orange == other.has_orange:
-                       return False
+               # Hypervisor
+               "hypervisor" : {
+                       "type" : "object",
+                       "properties" : {
+                               "vendor" : {
+                                       "type"    : "string",
+                                       "pattern" : r"^.{,40}$",
+                               },
+                       },
+                       "additionalProperties" : False,
+                       "required" : [
+                               "vendor",
+                       ],
+               },
 
-               if not self.has_blue == other.has_blue:
-                       return False
+               # Error - BogoMIPS
+               "bogomips" : {
+                       "type" : "number",
+               },
+       },
+       "additionalProperties" : False,
+       "required" : [
+               "cpu",
+               "devices",
+               "network",
+               "system",
+       ],
+}
 
-               return True
+class Network(Object):
+       def init(self, blob):
+               self.blob = blob
 
        def __iter__(self):
                ret = []
@@ -116,33 +300,28 @@ class ProfileNetwork(ProfileDict):
                return iter(ret)
 
        def has_zone(self, name):
-               return self._data.get("has_%s" % name)
+               return self.blob.get(name, False)
 
        @property
        def has_red(self):
-               return self._data.get("has_red", False)
+               return self.has_zone("red")
 
        @property
        def has_green(self):
-               return self._data.get("has_green", False)
+               return self.has_zone("green")
 
        @property
        def has_orange(self):
-               return self._data.get("has_orange", False)
+               return self.has_zone("orange")
 
        @property
        def has_blue(self):
-               return self._data.get("has_blue", False)
+               return self.has_zone("blue")
 
 
 class Processor(Object):
-       def __init__(self, backend, id, data=None, clock_speed=None, bogomips=None):
-               Object.__init__(self, backend)
-
-               self.id = id
-               self.__data = data
-               self.__clock_speed = clock_speed
-               self.__bogomips = bogomips
+       def init(self, blob):
+               self.blob = blob
 
        def __str__(self):
                s = []
@@ -158,43 +337,34 @@ class Processor(Object):
 
                return " ".join(s)
 
-       @property
-       def data(self):
-               if self.__data is None:
-                       self.__data = self.db.get("SELECT * FROM fireinfo_processors \
-                               WHERE id = %s", self.id)
-
-               return self.__data
-
        @property
        def vendor(self):
+               vendor = self.blob.get("vendor")
+
                try:
-                       return CPU_VENDORS[self.data.vendor]
+                       return CPU_VENDORS[vendor]
                except KeyError:
-                       return self.data.vendor
+                       return vendor
 
        @property
        def family(self):
-               return self.data.family
+               return self.blob.get("family")
 
        @property
        def model(self):
-               return self.data.model
+               return self.blob.get("model")
 
        @property
        def stepping(self):
-               return self.data.stepping
+               return self.blob.get("stepping")
 
        @property
        def model_string(self):
-               if self.data.model_string:
-                       s = self.data.model_string.split()
-
-                       return " ".join((e for e in s if e))
+               return self.blob.get("model_string")
 
        @property
        def flags(self):
-               return self.data.flags
+               return self.blob.get("flags")
 
        def has_flag(self, flag):
                return flag in self.flags
@@ -207,7 +377,7 @@ class Processor(Object):
 
        @property
        def core_count(self):
-               return self.data.core_count
+               return self.blob.get("count", 1)
 
        @property
        def count(self):
@@ -349,11 +519,8 @@ class Device(Object):
                }
        }
 
-       def __init__(self, backend, id, data=None):
-               Object.__init__(self, backend)
-
-               self.id = id
-               self.__data = data
+       def init(self, blob):
+               self.blob = blob
 
        def __repr__(self):
                return "<%s vendor=%s model=%s>" % (self.__class__.__name__,
@@ -361,7 +528,9 @@ class Device(Object):
 
        def __eq__(self, other):
                if isinstance(other, self.__class__):
-                       return self.id == other.id
+                       return self.blob == other.blob
+
+               return NotImplemented
 
        def __lt__(self, other):
                if isinstance(other, self.__class__):
@@ -371,53 +540,41 @@ class Device(Object):
                                self.model_string < other.model_string or \
                                self.model < other.model
 
-       @property
-       def data(self):
-               if self.__data is None:
-                       assert self.id
-
-                       self.__data = self.db.get("SELECT * FROM fireinfo_devices \
-                               WHERE id = %s", self.id)
-
-               return self.__data
+               return NotImplemented
 
        def is_showable(self):
-               if self.driver in IGNORED_DEVICES:
-                       return False
-
-               if self.driver in ("pcieport", "hub"):
+               if self.driver in ("usb", "pcieport", "hub"):
                        return False
 
                return True
 
        @property
        def subsystem(self):
-               return self.data.subsystem
+               return self.blob.get("subsystem")
 
        @property
        def model(self):
-               return self.data.model
+               return self.blob.get("model")
 
-       @property
+       @lazy_property
        def model_string(self):
-               return self.fireinfo.get_model_string(self.subsystem,
-                               self.vendor, self.model)
+               return self.fireinfo.get_model_string(self.subsystem, self.vendor, self.model)
 
        @property
        def vendor(self):
-               return self.data.vendor
+               return self.blob.get("vendor")
 
-       @property
+       @lazy_property
        def vendor_string(self):
                return self.fireinfo.get_vendor_string(self.subsystem, self.vendor)
 
        @property
        def driver(self):
-               return self.data.driver
+               return self.blob.get("driver")
 
-       @property
+       @lazy_property
        def cls(self):
-               classid = self.data.deviceclass
+               classid = self.blob.get("deviceclass")
 
                if self.subsystem == "pci":
                        classid = classid[:-4]
@@ -433,145 +590,81 @@ class Device(Object):
                except KeyError:
                        return "N/A"
 
-       @property
-       def percentage(self):
-               return self.data.get("percentage", None)
-
-
-class Profile(Object):
-       def __init__(self, backend, id, data=None):
-               Object.__init__(self, backend)
-
-               self.id = id
-               self.__data = data
 
-       def __repr__(self):
-               return "<%s %s>" % (self.__class__.__name__, self.public_id)
-
-       def __cmp__(self, other):
-               return cmp(self.id, other.id)
-
-       def is_showable(self):
-               if self.arch_id:
-                       return True
-
-               return False
+class System(Object):
+       def init(self, blob):
+               self.blob = blob
 
        @property
-       def data(self):
-               if self.__data is None:
-                       self.__data = self.db.get("SELECT * FROM fireinfo_profiles \
-                               WHERE id = %s", self.id)
-
-               return self.__data
+       def arch(self):
+               return self.blob.get("arch")
 
        @property
-       def public_id(self):
-               return self.data.public_id
+       def language(self):
+               return self.blob.get("language")
 
        @property
-       def private_id(self):
-               raise NotImplementedError
+       def vendor(self):
+               return self.blob.get("vendor")
 
        @property
-       def time_created(self):
-               return self.data.time_created
+       def model(self):
+               return self.blob.get("model")
 
        @property
-       def time_updated(self):
-               return self.data.time_updated
-
-       def updated(self, profile_parser=None, country_code=None, when=None):
-               valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
-               self.db.execute("UPDATE fireinfo_profiles \
-                       SET \
-                               time_updated = then_or_now(%s), \
-                               time_valid = then_or_now(%s) + INTERVAL '%s days', \
-                               updates = updates + 1 \
-                       WHERE id = %s", when, when, valid, self.id)
-
-               if profile_parser:
-                       self.set_processor_speeds(
-                               profile_parser.processor_clock_speed,
-                               profile_parser.processor_bogomips,
-                       )
-
-               if country_code:
-                       self.set_country_code(country_code)
-
-               self.log_profile_update()
-
-       def log_profile_update(self):
-               # Log that an update was performed for this profile id
-               self.db.execute("INSERT INTO fireinfo_profiles_log(public_id) \
-                       VALUES(%s)", self.public_id)
-
-       def expired(self, when=None):
-               self.db.execute("UPDATE fireinfo_profiles \
-                       SET time_valid = then_or_now(%s) WHERE id = %s", when, self.id)
+       def release(self):
+               return self.blob.get("release")
 
-       def parse(self, parser):
-               # Processor
-               self.processor = parser.processor
-               self.set_processor_speeds(parser.processor_clock_speed, parser.processor_bogomips)
+       @property
+       def storage(self):
+               return self.blob.get("storage_size", 0)
 
-               # All devices
-               self.devices = parser.devices
+       def is_virtual(self):
+               return self.blob.get("virtual", False)
 
-               # System
-               self.system_id = parser.system_id
 
-               # Memory
-               self.memory = parser.memory
+class Hypervisor(Object):
+       def init(self, blob):
+               self.blob = blob
 
-               # Storage
-               self.storage = parser.storage
+       def __str__(self):
+               return self.vendor
 
-               # Kernel
-               self.kernel_id = parser.kernel_id
+       @property
+       def vendor(self):
+               return self.blob.get("vendor")
 
-               # Arch
-               self.arch_id = parser.arch_id
 
-               # Release
-               self.release_id = parser.release_id
+class Profile(Object):
+       def init(self, profile_id, private_id, created_at, expired_at, version, blob,
+                       last_updated_at, country_code, **kwargs):
+               self.profile_id      = profile_id
+               self.private_id      = private_id
+               self.created_at      = created_at
+               self.expired_at      = expired_at
+               self.version         = version
+               self.blob            = blob
+               self.last_updated_at = last_updated_at
+               self.country_code    = country_code
 
-               # Language
-               self.language = parser.language
+       def __repr__(self):
+               return "<%s %s>" % (self.__class__.__name__, self.profile_id)
 
-               # Virtual
-               if parser.virtual:
-                       self.hypervisor_id = parser.hypervisor_id
+       def is_showable(self):
+               return True if self.blob else False
 
-               # Network
-               self.network = parser.network
+       @property
+       def public_id(self):
+               """
+                       An alias for the profile ID
+               """
+               return self.profile_id
 
        # Location
 
        @property
        def location(self):
-               if not hasattr(self, "_location"):
-                       res = self.db.get("SELECT location FROM fireinfo_profiles_locations \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._location = res.location
-                       else:
-                               self._location = None
-
-               return self._location
-
-       def set_country_code(self, country_code):
-               if self.location == country_code:
-                       return
-
-               self.db.execute("DELETE FROM fireinfo_profiles_locations \
-                       WHERE profile_id = %s", self.id)
-               self.db.execute("INSERT INTO fireinfo_profiles_locations(profile_id, location) \
-                       VALUES(%s, %s)", self.id, country_code)
-
-               self._location = country_code
+               return self.country_code
 
        @property
        def location_string(self):
@@ -579,1384 +672,597 @@ class Profile(Object):
 
        # Devices
 
-       @property
-       def device_ids(self):
-               if not hasattr(self, "_device_ids"):
-                       res = self.db.query("SELECT device_id FROM fireinfo_profiles_devices \
-                               WHERE profile_id = %s", self.id)
-
-                       self._device_ids = sorted([r.device_id for r in res])
-
-               return self._device_ids
-
-       def get_devices(self):
-               if not hasattr(self, "_devices"):
-                       res = self.db.query("SELECT * FROM fireinfo_devices \
-                               LEFT JOIN fireinfo_profiles_devices ON \
-                                       fireinfo_devices.id = fireinfo_profiles_devices.device_id \
-                               WHERE fireinfo_profiles_devices.profile_id = %s", self.id)
-
-                       self._devices = []
-                       for row in res:
-                               device = Device(self.backend, row.id, row)
-                               self._devices.append(device)
-
-               return self._devices
-
-       def set_devices(self, devices):
-               device_ids = [d.id for d in devices]
-
-               self.db.execute("DELETE FROM fireinfo_profiles_devices WHERE profile_id = %s", self.id)
-               self.db.executemany("INSERT INTO fireinfo_profiles_devices(profile_id, device_id) \
-                       VALUES(%s, %s)", ((self.id, d) for d in device_ids))
-
-               self._devices = devices
-               self._device_ids = device_ids
-
-       devices = property(get_devices, set_devices)
-
-       def count_device(self, subsystem, vendor, model):
-               counter = 0
-
-               for dev in self.devices:
-                       if dev.subsystem == subsystem and dev.vendor == vendor and dev.model == model:
-                               counter += 1
-
-               return counter
+       @lazy_property
+       def devices(self):
+               return [Device(self.backend, blob) for blob in self.blob.get("devices", [])]
 
        # System
 
-       def get_system_id(self):
-               if not hasattr(self, "_system_id"):
-                       res = self.db.get("SELECT system_id AS id FROM fireinfo_profiles_systems \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._system_id = res.id
-                       else:
-                               self._system_id = None
-
-               return self._system_id
-
-       def set_system_id(self, system_id):
-               self.db.execute("DELETE FROM fireinfo_profiles_systems WHERE profile_id = %s", self.id)
-
-               if system_id:
-                       self.db.execute("INSERT INTO fireinfo_profiles_systems(profile_id, system_id) \
-                               VALUES(%s, %s)", self.id, system_id)
-
-               self._system_id = None
-               if hasattr(self, "_system"):
-                       del self._system
-
-       system_id = property(get_system_id, set_system_id)
-
-       @property
+       @lazy_property
        def system(self):
-               if not hasattr(self, "_system"):
-                       res = self.db.get("SELECT fireinfo_systems.vendor AS vendor, fireinfo_systems.model AS model \
-                               FROM fireinfo_profiles_systems \
-                               LEFT JOIN fireinfo_systems ON fireinfo_profiles_systems.system_id = fireinfo_systems.id \
-                               WHERE fireinfo_profiles_systems.profile_id = %s", self.id)
-
-                       if res:
-                               self._system = (res.vendor, res.model)
-                       else:
-                               self._system = (None, None)
-
-               return self._system
-
-       @property
-       def system_vendor(self):
-               try:
-                       v, m = self.system
-                       return v
-               except TypeError:
-                       pass
-
-       @property
-       def system_model(self):
-               try:
-                       v, m = self.system
-                       return m
-               except TypeError:
-                       pass
-
-       @property
-       def appliance_id(self):
-               if not hasattr(self, "_appliance_id"):
-                       appliances = (
-                               ("fountainnetworks-duo-box", self._appliance_test_fountainnetworks_duo_box),
-                               ("fountainnetworks-prime", self._appliance_test_fountainnetworks_prime),
-                               ("lightningwirelabs-eco-plus", self._appliance_test_lightningwirelabs_eco_plus),
-                               ("lightningwirelabs-eco", self._appliance_test_lightningwirelabs_eco),
-                       )
+               return System(self.backend, self.blob.get("system", {}))
 
-                       self._appliance_id = None
-                       for name, test_func in appliances:
-                               if not test_func():
-                                       continue
-
-                               self._appliance_id = name
-                               break
-
-               return self._appliance_id
+       # Processor
 
        @property
-       def appliance(self):
-               if self.appliance_id == "fountainnetworks-duo-box":
-                       return "Fountain Networks - IPFire Duo Box"
-
-               elif self.appliance_id == "fountainnetworks-prime":
-                       return "Fountain Networks - IPFire Prime Box"
-
-               elif self.appliance_id == "lightningwirelabs-eco-plus":
-                       return "Lightning Wire Labs - IPFire Eco Plus Appliance"
-
-               elif self.appliance_id == "lightningwirelabs-eco":
-                       return "Lightning Wire Labs - IPFire Eco Appliance"
-
-       def _appliance_test_fountainnetworks_duo_box(self):
-               if not self.processor.vendor == "Intel":
-                       return False
-
-               if not self.processor.model_string == "Intel(R) Celeron(R) 2957U @ 1.40GHz":
-                       return False
-
-               if not self.count_device("pci", "10ec", "8168") == 2:
-                       return False
-
-               # WiFi module
-               #if self.count_device("usb", "148f", "5572") < 1:
-               #       return False
-
-               return True
-
-       def _appliance_test_fountainnetworks_prime(self):
-               if not self.system in (("SECO", None), ("SECO", "0949")):
-                       return False
-
-               # Must have a wireless device
-               if self.count_device("usb", "148f", "5572") < 1:
-                       return False
-
-               return True
-
-       def _appliance_test_lightningwirelabs_eco(self):
-               if not self.system == ("MSI", "MS-9877"):
-                       return False
-
-               # Must have four Intel network adapters
-               network_adapters_count = self.count_device("pci", "8086", "10d3")
-               if not network_adapters_count == 4:
-                       return False
-
-               return True
-
-       def _appliance_test_lightningwirelabs_eco_plus(self):
-               if not self.system_vendor == "ASUS":
-                       return False
-
-               if not self.system_model.startswith("P9A-I/2550"):
-                       return False
-
-               # Must have four Intel network adapters
-               network_adapters_count = self.count_device("pci", "8086", "1f41")
-               if not network_adapters_count == 4:
-                       return False
-
-               return True
-
-       # Processors
-
-       @property
-       def processor_id(self):
-               if hasattr(self, "_processor"):
-                       return self._processor.id
-
-               if not hasattr(self, "_processor_id"):
-                       res = self.db.get("SELECT processor_id FROM fireinfo_profiles_processors \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._processor_id = res.processor_id
-                       else:
-                               self._processor_id = None
-
-               return self._processor_id
-
-       def get_processor(self):
-               if not self.processor_id:
-                       return
-
-               if not hasattr(self, "_processor"):
-                       res = self.db.get("SELECT * FROM fireinfo_profiles_processors \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._processor = self.fireinfo.get_processor_by_id(res.processor_id,
-                                       clock_speed=res.clock_speed, bogomips=res.bogomips)
-                       else:
-                               self._processor = None
-
-               return self._processor
-
-       def set_processor(self, processor):
-               self.db.execute("DELETE FROM fireinfo_profiles_processors \
-                       WHERE profile_id = %s", self.id)
-
-               if processor:
-                       self.db.execute("INSERT INTO fireinfo_profiles_processors(profile_id, processor_id) \
-                               VALUES(%s, %s)", self.id, processor.id)
-
-               self._processor = processor
-
-       processor = property(get_processor, set_processor)
-
-       def set_processor_speeds(self, clock_speed, bogomips):
-               self.db.execute("UPDATE fireinfo_profiles_processors \
-                       SET clock_speed = %s, bogomips = %s WHERE profile_id = %s",
-                       clock_speed, bogomips, self.id)
-
-       # Compat
-       @property
-       def cpu(self):
-               return self.processor
+       def processor(self):
+               return Processor(self.backend, self.blob.get("cpu", {}))
 
        # Memory
 
-       def get_memory(self):
-               if not hasattr(self, "_memory"):
-                       res = self.db.get("SELECT amount FROM fireinfo_profiles_memory \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._memory = res.amount * 1024
-                       else:
-                               self._memory = None
-
-               return self._memory
-
-       def set_memory(self, amount):
-               if self.memory == amount:
-                       return
-
-               amount /= 1024
-
-               self.db.execute("DELETE FROM fireinfo_profiles_memory WHERE profile_id = %s", self.id)
-               if amount:
-                       self.db.execute("INSERT INTO fireinfo_profiles_memory(profile_id, amount) \
-                               VALUES(%s, %s)", self.id, amount)
-
-               self._memory = amount * 1024
-
-       memory = property(get_memory, set_memory)
+       @property
+       def memory(self):
+               return self.blob.get("memory")
 
        @property
        def friendly_memory(self):
                return util.format_size(self.memory or 0)
 
-       # Storage
-
-       def get_storage(self):
-               if not hasattr(self, "_storage"):
-                       res = self.db.get("SELECT amount FROM fireinfo_profiles_storage \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._storage = res.amount * 1024
-                       else:
-                               self._storage = None
-
-               return self._storage
-
-       def set_storage(self, amount):
-               if self.storage == amount:
-                       return
-
-               amount /= 1024
-
-               self.db.execute("DELETE FROM fireinfo_profiles_storage WHERE profile_id = %s", self.id)
-               if amount:
-                       self.db.execute("INSERT INTO fireinfo_profiles_storage(profile_id, amount) \
-                               VALUES(%s, %s)", self.id, amount)
-
-               self._storage = amount * 1024
-
-       storage = property(get_storage, set_storage)
-
-       @property
-       def friendly_storage(self):
-               return util.format_size(self.storage)
-
-       # Kernel
-
-       def get_kernel_id(self):
-               if not hasattr(self, "_kernel_id"):
-                       res = self.db.get("SELECT fireinfo_profiles_kernels.kernel_id AS id FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._kernel_id = res.id
-                       else:
-                               self._kernel_id = None
-
-               return self._kernel_id
-
-       def set_kernel_id(self, kernel_id):
-               if self.kernel_id == kernel_id:
-                       return
-
-               self.db.execute("DELETE FROM fireinfo_profiles_kernels WHERE profile_id = %s", self.id)
-               if kernel_id:
-                       self.db.execute("INSERT INTO fireinfo_profiles_kernels(profile_id, kernel_id) \
-                               VALUES(%s, %s)", self.id, kernel_id)
-
-               self._kernel_id = kernel_id
-               if hasattr(self, "_kernel"):
-                       del self._kernel
-
-       kernel_id = property(get_kernel_id, set_kernel_id)
-
-       @property
-       def kernel(self):
-               if not hasattr(self, "_kernel"):
-                       res = self.db.get("SELECT fireinfo_kernels.name AS name FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_kernels ON fireinfo_profiles.id = fireinfo_profiles_kernels.profile_id \
-                               LEFT JOIN fireinfo_kernels ON fireinfo_kernels.id = fireinfo_profiles_kernels.kernel_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._kernel = res.name
-                       else:
-                               self._kernel = None
-
-               return self._kernel
-
-       # Arch
-
-       def get_arch_id(self):
-               if not hasattr(self, "_arch_id"):
-                       res = self.db.get("SELECT fireinfo_profiles_arches.arch_id AS id FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._arch_id = res.id
-                       else:
-                               self._arch_id = None
-
-               return self._arch_id
-
-       def set_arch_id(self, arch_id):
-               if self.arch_id == arch_id:
-                       return
-
-               self.db.execute("DELETE FROM fireinfo_profiles_arches WHERE profile_id = %s", self.id)
-               if arch_id:
-                       self.db.execute("INSERT INTO fireinfo_profiles_arches(profile_id, arch_id) \
-                               VALUES(%s, %s)", self.id, arch_id)
-
-               self._arch_id = None
-               if hasattr(self, "_arch"):
-                       del self._arch
-
-       arch_id = property(get_arch_id, set_arch_id)
-
-       @property
-       def arch(self):
-               if not hasattr(self, "_arch"):
-                       res = self.db.get("SELECT fireinfo_arches.name AS name FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
-                               LEFT JOIN fireinfo_arches ON fireinfo_arches.id = fireinfo_profiles_arches.arch_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._arch = res.name
-                       else:
-                               self._arch = None
-
-               return self._arch
-
-       # Release
-
-       def get_release_id(self):
-               if not hasattr(self, "_release_id"):
-                       res = self.db.get("SELECT fireinfo_profiles_releases.release_id AS id FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._release_id = res.id
-                       else:
-                               self._release_id = None
-
-               return self._release_id
-
-       def set_release_id(self, release_id):
-               if self.release_id == release_id:
-                       return
-
-               self.db.execute("DELETE FROM fireinfo_profiles_releases WHERE profile_id = %s", self.id)
-               if release_id:
-                       self.db.execute("INSERT INTO fireinfo_profiles_releases(profile_id, release_id) \
-                               VALUES(%s, %s)", self.id, release_id)
-
-               self._release_id = release_id
-               if hasattr(self, "_release"):
-                       del self._release
-
-       release_id = property(get_release_id, set_release_id)
-
-       @property
-       def release(self):
-               if not hasattr(self, "_release"):
-                       res = self.db.get("SELECT fireinfo_releases.name AS name FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_releases ON fireinfo_profiles.id = fireinfo_profiles_releases.profile_id \
-                               LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._release = self._format_release(res.name)
-                       else:
-                               self._release = None
-
-               return self._release
-
-       @staticmethod
-       def _format_release(r):
-               if not r:
-                       return r
-
-               # Remove the development header
-               r = r.replace("Development Build: ", "")
-
-               pairs = (
-                       ("-beta", " - Beta "),
-                       ("-rc", " - Release Candidate "),
-                       ("core", "Core Update "),
-                       ("beta", "Beta "),
-               )
-
-               for k, v in pairs:
-                       r = r.replace(k, v)
-
-               return r
-
-       @property
-       def release_short(self):
-               pairs = (
-                       (r"Release Candidate (\d+)", r"RC\1"),
-               )
-
-               s = self.release
-               for pattern, repl in pairs:
-                       if re.search(pattern, s) is None:
-                               continue
-
-                       s = re.sub(pattern, repl, s)
-
-               return s
-
        # Virtual
 
-       @property
-       def virtual(self):
-               if not hasattr(self, "_virtual"):
-                       res = self.db.get("SELECT 1 FROM fireinfo_profiles_virtual \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._virtual = True
-                       else:
-                               self._virtual = False
-
-               return self._virtual
-
-       def get_hypervisor_id(self):
-               if not hasattr(self, "_hypervisor_id"):
-                       res = self.db.get("SELECT fireinfo_profiles_virtual.hypervisor_id AS id FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._hypervisor_id = res.id
-                       else:
-                               self._hypervisor_id = None
-
-               return self._hypervisor_id
-
-       def set_hypervisor_id(self, hypervisor_id):
-               self.db.execute("DELETE FROM fireinfo_profiles_virtual WHERE profile_id = %s", self.id)
-               self.db.execute("INSERT INTO fireinfo_profiles_virtual(profile_id, hypervisor_id) \
-                       VALUES(%s, %s)", self.id, hypervisor_id)
-
-               self._hypervisor_id = hypervisor_id
-
-       hypervisor_id = property(get_hypervisor_id, set_hypervisor_id)
+       def is_virtual(self):
+               return self.system.is_virtual()
 
        @property
        def hypervisor(self):
-               if not hasattr(self, "_hypervisor"):
-                       res = self.db.get("SELECT fireinfo_hypervisors.name AS hypervisor FROM fireinfo_profiles \
-                               LEFT JOIN fireinfo_profiles_virtual ON fireinfo_profiles.id = fireinfo_profiles_virtual.profile_id \
-                               LEFT JOIN fireinfo_hypervisors ON fireinfo_profiles_virtual.hypervisor_id = fireinfo_hypervisors.id \
-                               WHERE fireinfo_profiles.id = %s", self.id)
-
-                       if res:
-                               self._hypervisor = res.hypervisor
-                       else:
-                               self._hypervisor = None
-
-               return self._hypervisor
-
-       # Language
-
-       def get_language(self):
-               if not hasattr(self, "_language"):
-                       res = self.db.get("SELECT language FROM fireinfo_profiles_languages \
-                               WHERE profile_id = %s", self.id)
-
-                       if res:
-                               self._language = res.language
-                       else:
-                               self._language = None
-
-               return self._language
-
-       def set_language(self, language):
-               self.db.execute("DELETE FROM fireinfo_profiles_languages WHERE profile_id = %s", self.id)
-
-               if language:
-                       self.db.execute("INSERT INTO fireinfo_profiles_languages(profile_id, language) \
-                               VALUES(%s, %s)", self.id, language)
-
-               self._language = language
-
-       language = property(get_language, set_language)
+               return Hypervisor(self.backend, self.blob.get("hypervisor"))
 
        # Network
 
-       def get_network(self):
-               if not hasattr(self, "_network"):
-                       res = self.db.get("SELECT * FROM fireinfo_profiles_networks \
-                               WHERE profile_id = %s", self.id)
-
-                       if not res:
-                               res = {}
-
-                       self._network = ProfileNetwork(res)
-
-               return self._network
-
-       def set_network(self, network):
-               self.db.execute("DELETE FROM fireinfo_profiles_networks WHERE profile_id = %s", self.id)
-
-               if network:
-                       self.db.execute("INSERT INTO fireinfo_profiles_networks(profile_id, \
-                               has_red, has_green, has_orange, has_blue) VALUES(%s, %s, %s, %s, %s)",
-                               self.id, network.has_red, network.has_green, network.has_orange, network.has_blue)
-
-               self._network = network
-
-       network = property(get_network, set_network)
-
-
-class ProfileData(Object):
-       def __init__(self, backend, id, data=None, profile=None):
-               Object.__init__(self, backend)
-
-               self.id = id
-               self._data = data
-               self._profile = profile
-
-       @property
-       def data(self):
-               if self._data is None:
-                       self._data = self.db.get("SELECT * FROM fireinfo_profile_data \
-                               WHERE id = %s", self.id)
-
-               return self._data
-
-       @property
-       def profile(self):
-               if not self._profile:
-                       self._profile = self.fireinfo.get_profile_by_id(self.profile_id)
-
-               return self._profile
-
-       @property
-       def profile_id(self):
-               return self.data.profile_id
-
-
-class ProfileParserError(Exception):
-       pass
-
-
-class ProfileParser(Object):
-       __device_args = (
-               "subsystem",
-               "vendor",
-               "model",
-               "sub_vendor",
-               "sub_model",
-               "driver",
-               "deviceclass",
-       )
-
-       __processor_args = (
-               "vendor",
-               "model_string",
-               "family",
-               "model",
-               "stepping",
-               "core_count",
-               "flags",
-       )
-
-       def __init__(self, backend, public_id, blob=None):
-               Object.__init__(self, backend)
-
-               self.public_id = public_id
-               self.private_id = None
-               self.devices = []
-               self.processor = None
-               self.processor_clock_speed = None
-               self.processor_bogomips = None
-               self.system_id = None
-               self.memory = None
-               self.storage = None
-               self.kernel = None
-               self.kernel_id = None
-               self.arch = None
-               self.arch_id = None
-               self.release = None
-               self.release_id = None
-               self.language = None
-               self.virtual = None
-               self.hypervisor_id = None
-               self.network = None
-
-               self.__parse_blob(blob)
-
-       def equals(self, other):
-               if not self.processor_id == other.processor_id:
-                       return False
-
-               if not self.device_ids == other.device_ids:
-                       return False
-
-               if not self.system_id == other.system_id:
-                       return False
-
-               if not self.memory == other.memory:
-                       return False
-
-               if not self.storage == other.storage:
-                       return False
-
-               if not self.kernel_id == other.kernel_id:
-                       return False
-
-               if not self.arch_id == other.arch_id:
-                       return False
-
-               if not self.release_id == other.release_id:
-                       return False
-
-               if not self.language == other.language:
-                       return False
-
-               if not self.virtual == other.virtual:
-                       return False
-
-               if other.virtual:
-                       if not self.hypervisor_id == other.hypervisor_id:
-                               return False
-
-               if not self.network == other.network:
-                       return False
-
-               return True
-
-       def __parse_blob(self, blob):
-               _profile = blob.get("profile", {})
-               self.private_id = blob.get("private_id")
-
-               # Do not try to parse an empty profile
-               if not _profile:
-                       return
-
-               # Processor
-               _processor = _profile.get("cpu", {})
-               self.__parse_processor(_processor)
-
-               # Find devices
-               _devices = _profile.get("devices", [])
-               self.__parse_devices(_devices)
-
-               # System
-               _system = _profile.get("system")
-               if _system:
-                       self.__parse_system(_system)
-
-                       # Memory (convert to bytes)
-                       memory = _system.get("memory", None)
-                       if memory:
-                               self.memory = memory * 1024
-
-                       # Storage size (convert to bytes)
-                       storage = _system.get("root_size", None)
-                       if storage:
-                               self.storage = storage * 1024
-
-                       # Kernel
-                       kernel = _system.get("kernel_release", None)
-                       if kernel:
-                               self.__parse_kernel(kernel)
-
-                       # Release
-                       release = _system.get("release", None)
-                       if release:
-                               self.__parse_release(release)
-
-                       # Language
-                       language = _system.get("language", None)
-                       if language:
-                               self.__parse_language(language)
-
-                       # Virtual
-                       self.virtual = _system.get("virtual", False)
-                       if self.virtual:
-                               hypervisor = _profile.get("hypervisor")
-                               self.__parse_hypervisor(hypervisor)
-
-               # Network
-               _network = _profile.get("network")
-               if _network:
-                       self.__parse_network(_network)
-
-       @property
-       def device_ids(self):
-               return sorted([d.id for d in self.devices])
-
-       def __parse_devices(self, _devices):
-               self.devices = []
-
-               for _device in _devices:
-                       args = {}
-                       for arg in self.__device_args:
-                               args[arg] = _device.get(arg, None)
-
-                       # Skip if the subsystem is not set
-                       if not args.get("subsystem", None):
-                               continue
-
-                       # Find the device or create a new one.
-                       device = self.fireinfo.get_device(**args)
-                       if not device:
-                               device = self.fireinfo.create_device(**args)
-
-                       self.devices.append(device)
-
-       def __parse_system(self, system):
-               vendor = system.get("vendor", None)
-               if not vendor:
-                       vendor = None
-
-               model = system.get("model", None)
-               if not model:
-                       model = None
-
-               self.system_id = self.fireinfo.get_system(vendor, model)
-               if not self.system_id:
-                       self.system_id = self.fireinfo.create_system(vendor, model)
-
-       @property
-       def processor_id(self):
-               if not self.processor:
-                       return
-
-               return self.processor.id
-
-       def __parse_processor(self, _processor):
-               args = {}
-               for arg in self.__processor_args:
-                       if arg == "core_count":
-                               _arg = "count"
-                       else:
-                               _arg = arg
-
-                       args[arg] = _processor.get(_arg, None)
-
-               self.processor = self.fireinfo.get_processor(**args)
-               if not self.processor:
-                       self.processor = self.fireinfo.create_processor(**args)
-
-               self.processor_clock_speed = _processor.get("speed", None)
-               self.processor_bogomips = _processor.get("bogomips", None)
-
-               arch = _processor.get("arch", None)
-               if arch:
-                       self.__parse_arch(arch)
-
-       def __parse_kernel(self, kernel):
-               self.kernel_id = self.fireinfo.get_kernel(kernel)
-               if not self.kernel_id:
-                       self.kernel_id = self.fireinfo.create_kernel(kernel)
-                       assert self.kernel_id
-
-               self.kernel = kernel
-
-       def __parse_arch(self, arch):
-               self.arch_id = self.fireinfo.get_arch(arch)
-               if not self.arch_id:
-                       self.arch_id = self.fireinfo.create_arch(arch)
-
-               self.arch = arch
-
-       def __parse_release(self, release):
-               # Remove the arch bit
-               if release:
-                       r = [e for e in release.split() if e]
-                       for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
-                               try:
-                                       r.remove(s)
-                                       break
-                               except ValueError:
-                                       pass
-
-                       release = " ".join(r)
-
-               self.release_id = self.fireinfo.get_release(release)
-               if not self.release_id:
-                       self.release_id = self.fireinfo.create_release(release)
-                       assert self.release_id
-
-               self.release = release
-
-       def __parse_language(self, language):
-               self.language = language
-               self.language, delim, rest = self.language.partition(".")
-               self.language, delim, rest = self.language.partition("_")
-
-       def __parse_hypervisor(self, hypervisor):
-               vendor = hypervisor.get("vendor", "other")
-
-               if vendor in ("other", "unknown"):
-                       self.hypervisor_id = None
-                       return
-
-               self.hypervisor_id = self.fireinfo.get_hypervisor(vendor)
-               if not self.hypervisor_id:
-                       self.hypervisor_id = self.fireinfo.create_hypervisor(vendor)
-
-       def __parse_network(self, network):
-               self.network = ProfileNetwork({
-                       "has_red"    : network.get("red", False),
-                       "has_green"  : network.get("green", False),
-                       "has_orange" : network.get("orange", False),
-                       "has_blue"   : network.get("blue", False),
-               })
+       @lazy_property
+       def network(self):
+               return Network(self.backend, self.blob.get("network", {}))
 
 
 class Fireinfo(Object):
-       def get_profile_count(self, when=None):
-               res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
-                       WHERE then_or_now(%s) BETWEEN time_created AND time_valid", when)
-
-               if res:
-                       return res.count
-
-       def get_total_updates_count(self, when=None):
-               res = self.db.get("SELECT COUNT(*) + SUM(updates) AS count \
-                       FROM fireinfo_profiles WHERE time_created <= then_or_now(%s)", when)
-
-               if res:
-                       return res.count
-
-       # Parser
-
-       def parse_profile(self, public_id, blob):
-               return ProfileParser(self.backend, public_id, blob)
-
-       # Profiles
-
-       def profile_exists(self, public_id):
-               res = self.db.get("SELECT id FROM fireinfo_profiles \
-                       WHERE public_id = %s LIMIT 1", public_id)
-
-               if res:
-                       return True
-
-               return False
-
-       def profile_rate_limit_active(self, public_id, when=None):
-               res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles_log \
-                       WHERE public_id = %s AND ts >= then_or_now(%s) - INTERVAL '60 minutes'",
-                        public_id, when)
-
-               if res and res.count >= 10:
-                       return True
-
-               return False
-
-       def is_private_id_change_permitted(self, public_id, private_id, when=None):
-               # Check if a profile exists with a different private id that is still valid
-               res = self.db.get("SELECT 1 FROM fireinfo_profiles \
-                       WHERE public_id = %s AND NOT private_id = %s \
-                       AND time_valid >= then_or_now(%s) LIMIT 1", public_id, private_id, when)
-
-               if res:
-                       return False
-
-               return True
-
-       def get_profile(self, public_id, private_id=None, when=None):
-               res = self.db.get("SELECT * FROM fireinfo_profiles \
-                       WHERE public_id = %s AND \
-                               (CASE WHEN %s IS NULL THEN TRUE ELSE private_id = %s END) AND \
-                               then_or_now(%s) BETWEEN time_created AND time_valid \
-                       ORDER BY time_updated DESC LIMIT 1",
-                       public_id, private_id, private_id, when)
-
-               if res:
-                       return Profile(self.backend, res.id, res)
+       async def expire(self):
+               """
+                       Called to expire any profiles that have not been updated in a fortnight
+               """
+               self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+                       WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime.timedelta(days=14))
 
-       def get_profile_with_data(self, public_id, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT * FROM profiles JOIN fireinfo_profiles ON profiles.id = fireinfo_profiles.id \
-                               WHERE public_id = %s ORDER BY time_updated DESC LIMIT 1", when, public_id)
+       def _get_profile(self, query, *args, **kwargs):
+               res = self.db.get(query, *args, **kwargs)
 
                if res:
-                       return Profile(self.backend, res.id, res)
+                       return Profile(self.backend, **res)
 
-       def get_profiles(self, public_id):
-               res = self.db.query("SELECT * FROM fireinfo_profiles \
-                       WHERE public_id = %s ORDER BY time_created DESC", public_id)
-
-               profiles = []
-               for row in res:
-                       profile = Profile(self.backend, row.id, row)
-                       profiles.append(profile)
-
-               return profiles
-
-       def create_profile(self, public_id, private_id, when=None):
-               valid = self.settings.get_int("fireinfo_profile_days_valid", 14)
-
-               res = self.db.get("INSERT INTO fireinfo_profiles(public_id, private_id, \
-                       time_created, time_updated, time_valid) VALUES(%s, %s, then_or_now(%s), \
-                       then_or_now(%s), then_or_now(%s) + INTERVAL '%s days') RETURNING id",
-                       public_id, private_id, when, when, when, valid)
-
-               if res:
-                       p = Profile(self.backend, res.id)
-                       p.log_profile_update()
-
-                       return p
-
-       # Devices
-
-       def create_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
-                       driver=None, deviceclass=None):
-               res = self.db.get("INSERT INTO fireinfo_devices(subsystem, vendor, model, \
-                               sub_vendor, sub_model, driver, deviceclass) VALUES(%s, %s, %s, %s, %s, %s, %s) \
-                               RETURNING id", subsystem, vendor, model, sub_vendor, sub_model, driver, deviceclass)
-
-               if res:
-                       return Device(self.backend, res.id)
-
-       def get_device(self, subsystem, vendor, model, sub_vendor=None, sub_model=None,
-                       driver=None, deviceclass=None):
-               res = self.db.get("SELECT * FROM fireinfo_devices \
-                       WHERE subsystem = %s AND vendor = %s AND model = %s \
-                       AND sub_vendor IS NOT DISTINCT FROM %s \
-                       AND sub_model IS NOT DISTINCT FROM %s \
-                       AND driver IS NOT DISTINCT FROM %s \
-                       AND deviceclass IS NOT DISTINCT FROM %s \
-                       LIMIT 1", subsystem, vendor, model, sub_vendor,
-                               sub_model, driver, deviceclass)
-
-               if res:
-                       return Device(self.backend, res.id, res)
-
-       # System
-
-       def create_system(self, vendor, model):
-               res = self.db.get("INSERT INTO fireinfo_systems(vendor, model) \
-                       VALUES(%s, %s) RETURNING id", vendor, model)
-
-               if res:
-                       return res.id
-
-       def get_system(self, vendor, model):
-               res = self.db.get("SELECT id FROM fireinfo_systems WHERE vendor IS NOT DISTINCT FROM %s \
-                       AND model IS NOT DISTINCT FROM %s LIMIT 1", vendor, model)
-
-               if res:
-                       return res.id
-
-       # Processors
-
-       def create_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
-               res = self.db.get("INSERT INTO fireinfo_processors(vendor, model_string, \
-                       family, model, stepping, core_count, flags) VALUES(%s, %s, %s, %s, %s, %s, %s) \
-                       RETURNING id", vendor or None, model_string or None, family, model, stepping, core_count, flags)
-
-               if res:
-                       return Processor(self.backend, res.id)
-
-       def get_processor_by_id(self, processor_id, **kwargs):
-               res = self.db.get("SELECT * FROM fireinfo_processors \
-                       WHERE id = %s", processor_id)
-
-               if res:
-                       return Processor(self.backend, res.id, data=res, **kwargs)
-
-       def get_processor(self, vendor, model_string, family, model, stepping, core_count, flags=None):
-               if flags is None:
-                       flags = []
-
-               res = self.db.get("SELECT * FROM fireinfo_processors \
-                       WHERE vendor IS NOT DISTINCT FROM %s AND model_string IS NOT DISTINCT FROM %s \
-                       AND family IS NOT DISTINCT FROM %s AND model IS NOT DISTINCT FROM %s \
-                       AND stepping IS NOT DISTINCT FROM %s AND core_count = %s \
-                       AND flags <@ %s AND flags @> %s", vendor or None, model_string or None,
-                       family, model, stepping, core_count, flags, flags)
-
-               if res:
-                       return Processor(self.backend, res.id, res)
-
-       # Kernel
-
-       def create_kernel(self, kernel):
-               res = self.db.get("INSERT INTO fireinfo_kernels(name) VALUES(%s) \
-                       RETURNING id", kernel)
-
-               if res:
-                       return res.id
-
-       def get_kernel(self, kernel):
-               res = self.db.get("SELECT id FROM fireinfo_kernels WHERE name = %s", kernel)
-
-               if res:
-                       return res.id
-
-       # Arch
+       def get_profile_count(self, when=None):
+               if when:
+                       res = self.db.get("""
+                               SELECT
+                                       COUNT(*) AS count
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       created_at <= %s
+                               AND
+                                       (
+                                               expired_at IS NULL
+                                       OR
+                                               expired_at > %s
+                                       )
+                       """)
+               else:
+                       res = self.db.get("""
+                               SELECT
+                                       COUNT(*) AS count
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               """,
+                       )
 
-       def create_arch(self, arch):
-               res = self.db.get("INSERT INTO fireinfo_arches(name) VALUES(%s) \
-                       RETURNING id", arch)
+               return res.count if res else 0
 
-               if res:
-                       return res.id
+       def get_profile_histogram(self):
+               today = datetime.date.today()
 
-       def get_arch(self, arch):
-               res = self.db.get("SELECT id FROM fireinfo_arches WHERE name = %s", arch)
+               t1 = datetime.date(year=today.year - 10, month=today.month, day=1)
+               t2 = datetime.date(year=today.year, month=today.month, day=1)
 
-               if res:
-                       return res.id
+               res = self.db.query("""
+                       SELECT
+                               date,
+                               COUNT(*) AS count
+                       FROM
+                               generate_series(%s, %s, INTERVAL '1 month') date
+                       JOIN
+                               fireinfo ON date >= created_at
+                                       AND (expired_at IS NULL OR expired_at > date)
+                       GROUP BY
+                               date
+               """, t1, t2)
 
-       # Release
+               return { row.date : row.count for row in res }
 
-       def create_release(self, release):
-               res = self.db.get("INSERT INTO fireinfo_releases(name) VALUES(%s) \
-                       RETURNING id", release)
+       # Profiles
 
-               if res:
-                       return res.id
+       def get_profile(self, profile_id, when=None):
+               if when:
+                       return self._get_profile("""
+                               SELECT
+                                       *
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       profile_id = %s
+                               AND
+                                       %s BETWEEN created_at AND expired_at
+                               """, profile_id,
+                       )
 
-       def get_release(self, release):
-               res = self.db.get("SELECT id FROM fireinfo_releases WHERE name = %s", release)
+               return self._get_profile("""
+                       SELECT
+                               *
+                       FROM
+                               fireinfo
+                       WHERE
+                               profile_id = %s
+                       AND
+                               expired_at IS NULL
+                       """, profile_id,
+               )
 
-               if res:
-                       return res.id
+       # Handle profile
 
-       def get_release_penetration(self, release, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS penetration FROM profiles \
-                       LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
-                       WHERE fireinfo_profiles_releases.release_id = %s", when, release.fireinfo_id)
+       def handle_profile(self, profile_id, blob, country_code=None, when=None):
+               private_id = blob.get("private_id", None)
+               assert private_id
 
-               if res:
-                       return res.penetration
+               now = datetime.datetime.utcnow()
 
-       def get_random_country_penetration(self):
-               res = self.db.get("SELECT * FROM fireinfo_country_percentages \
-                       ORDER BY RANDOM() LIMIT 1")
+               # Fetch the profile version
+               version = blob.get("profile_version")
 
-               if res:
-                       return database.Row({
-                               "country"    : iso3166.countries.get(res.location),
-                               "percentage" : res.count,
-                       })
+               # Extract the profile
+               profile = blob.get("profile")
 
-       # Hypervisor
+               # Validate the profile
+               self._validate(profile_id, version, profile)
 
-       def create_hypervisor(self, hypervisor):
-               res = self.db.get("INSERT INTO fireinfo_hypervisors(name) VALUES(%s) \
-                       RETURNING id", hypervisor)
+               # Pre-process the profile
+               profile = self._preprocess(profile)
 
-               if res:
-                       return res.id
+               # Fetch the previous profile
+               prev = self.get_profile(profile_id)
 
-       def get_hypervisor(self, hypervisor):
-               res = self.db.get("SELECT id FROM fireinfo_hypervisors WHERE name = %s",
-                       hypervisor)
+               if prev:
+                       # Check if the private ID matches
+                       if not prev.private_id == private_id:
+                               logging.error("Private ID for profile %s does not match" % profile_id)
+                               return False
 
-               if res:
-                       return res.id
+                       # Check when the last update was
+                       elif now - prev.last_updated_at < datetime.timedelta(hours=6):
+                               logging.warning("Profile %s has been updated too soon" % profile_id)
+                               return False
 
-       # Handle profile
+                       # Check if the profile has changed
+                       elif prev.version == version and prev.blob == blob:
+                               logging.debug("Profile %s has not changed" % profile_id)
+
+                               # Update the timestamp
+                               self.db.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
+                                       WHERE profile_id = %s AND expired_at IS NULL", profile_id)
+
+                               return True
+
+                       # Delete the previous profile
+                       self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
+                               WHERE profile_id = %s AND expired_at IS NULL", profile_id)
+
+               # Store the new profile
+               self.db.execute("""
+                       INSERT INTO
+                               fireinfo
+                       (
+                               profile_id,
+                               private_id,
+                               version,
+                               blob,
+                               country_code
+                       )
+                       VALUES
+                       (
+                               %s,
+                               %s,
+                               %s,
+                               %s,
+                               %s
+                       )
+                       """, profile_id, private_id, version, json.dumps(profile), country_code,
+               )
 
-       def handle_profile(self, *args, **kwargs):
-               self.db.execute("START TRANSACTION")
+       def _validate(self, profile_id, version, blob):
+               """
+                       Validate the profile
+               """
+               if not version == 0:
+                       raise ValueError("Unsupported profile version")
 
-               # Wrap all the handling of the profile in a huge transaction.
+               # Validate the blob
                try:
-                       self._handle_profile(*args, **kwargs)
-
-               except:
-                       self.db.execute("ROLLBACK")
-                       raise
-
-               else:
-                       self.db.execute("COMMIT")
-
-       def _handle_profile(self, public_id, profile_blob, country_code=None, when=None):
-               private_id = profile_blob.get("private_id", None)
-               assert private_id
-
-               # Check if the profile already exists in the database.
-               profile = self.fireinfo.get_profile(public_id, private_id=private_id, when=when)
+                       return jsonschema.validate(blob, schema=PROFILE_SCHEMA)
 
-               # Check if the update can actually be updated
-               if profile and self.fireinfo.profile_rate_limit_active(public_id, when=when):
-                       logging.warning("There were too many updates for this profile in the last hour: %s" % public_id)
-                       return
-
-               elif not self.is_private_id_change_permitted(public_id, private_id, when=when):
-                       logging.warning("Changing private id is not permitted for profile: %s" % public_id)
-                       return
-
-               # Parse the profile
-               profile_parser = self.parse_profile(public_id, profile_blob)
+               # Raise a ValueError instead which is easier to handle later on
+               except jsonschema.exceptions.ValidationError as e:
+                       raise ValueError("%s" % e) from e
 
-               # If a profile exists, check if it matches and if so, just update the
-               # timestamp.
-               if profile:
-                       # Check if the profile has changed. If so, update the data.
-                       if profile_parser.equals(profile):
-                               profile.updated(profile_parser, country_code=country_code, when=when)
-                               return
+       def _preprocess(self, blob):
+               """
+                       Modifies the profile before storing it
+               """
+               # Remove the architecture from the release string
+               blob["system"]["release"]= self._filter_release(blob["system"]["release"])
 
-                       # If it does not match, we assume that it is expired and
-                       # create a new profile.
-                       profile.expired(when=when)
+               return blob
 
-               # Replace the old profile with a new one
-               profile = self.fireinfo.create_profile(public_id, private_id, when=when)
-               profile.parse(profile_parser)
+       def _filter_release(self, release):
+               """
+                       Removes the arch part
+               """
+               r = [e for e in release.split() if e]
 
-               if country_code:
-                       profile.set_country_code(country_code)
+               for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
+                       try:
+                               r.remove(s)
+                               break
+                       except ValueError:
+                               pass
 
-               return profile
+               return " ".join(r)
 
        # Data outputs
 
        def get_random_profile(self, when=None):
-               # Check if the architecture exists so that we pick a profile with some data
-               res = self.db.get("SELECT public_id FROM fireinfo_profiles \
-                       LEFT JOIN fireinfo_profiles_arches ON fireinfo_profiles.id = fireinfo_profiles_arches.profile_id \
-                       WHERE fireinfo_profiles_arches.profile_id IS NOT NULL \
-                       AND then_or_now(%s) BETWEEN time_created AND time_valid ORDER BY RANDOM() LIMIT 1", when)
+               if when:
+                       return self._get_profile("""
+                               SELECT
+                                       *
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       created_at <= %s
+                               AND
+                                       (
+                                               expired_at IS NULL
+                                       OR
+                                               expired_at > %s
+                                       )
+                               ORDER BY
+                                       RANDOM()
+                               LIMIT
+                                       1
+                               """, when, when,
+                       )
 
-               if res:
-                       return res.public_id
+               return self._get_profile("""
+                       SELECT
+                               *
+                       FROM
+                               fireinfo
+                       WHERE
+                               expired_at IS NULL
+                       ORDER BY
+                               RANDOM()
+                       LIMIT
+                               1
+               """)
 
        def get_active_profiles(self, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
-                       SELECT COUNT(*) AS with_data, (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
-                       WHERE fireinfo_profiles_releases.profile_id IS NOT NULL", when)
+               if when:
+                       raise NotImplementedError
 
-               if res:
-                       return res.with_data, res.count
-
-       def get_archive_size(self, when=None):
-               res = self.db.get("SELECT COUNT(*) AS count FROM fireinfo_profiles \
-                       WHERE time_created <= then_or_now(%s)", when)
+               else:
+                       res = self.db.get("""
+                               SELECT
+                                       COUNT(*) AS total_profiles,
+                                       COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                       """)
 
                if res:
-                       return res.count
-
-       def get_geo_location_map(self, when=None, minimum_percentage=0):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_at(%s) AS id) \
-                       SELECT location, COUNT(location)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_locations ON profiles.id = fireinfo_profiles_locations.profile_id \
-                       WHERE fireinfo_profiles_locations.location IS NOT NULL GROUP BY location \
-                       HAVING COUNT(location)::float / (SELECT COUNT(*) FROM profiles) >= %s ORDER BY count DESC",
-                       when, minimum_percentage)
-
-               return list(((r.location, r.count) for r in res))
+                       return res.active_profiles, res.total_profiles
+
+       def get_geo_location_map(self, when=None):
+               if when:
+                       res = self.db.query("""
+                               SELECT
+                                       country_code,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       created_at <= %s
+                               AND
+                                       (
+                                               expired_at IS NULL
+                                       OR
+                                               expired_at > %s
+                                       )
+                               AND
+                                       country_code IS NOT NULL
+                               GROUP BY
+                                       country_code
+                       """, when, when)
+               else:
+                       res = self.db.query("""
+                               SELECT
+                                       country_code,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       country_code IS NOT NULL
+                               GROUP BY
+                                       country_code
+                       """)
+
+               return { row.country_code : row.p for row in res }
 
        @property
        def cpu_vendors(self):
-               res = self.db.query("SELECT DISTINCT vendor FROM fireinfo_processors ORDER BY vendor")
+               res = self.db.query("""
+                       SELECT DISTINCT
+                               blob->'cpu'->'vendor' AS vendor
+                       FROM
+                               fireinfo
+                       WHERE
+                               blob->'cpu'->'vendor' IS NOT NULL
+                       """,
+               )
 
-               return (CPU_VENDORS.get(r.vendor, r.vendor) for r in res)
+               return sorted((CPU_VENDORS.get(row.vendor, row.vendor) for row in res))
 
        def get_cpu_vendors_map(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT COALESCE(vendor, %s) AS vendor, COUNT(vendor)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
-                       LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
-                       WHERE NOT fireinfo_profiles_processors.processor_id IS NULL GROUP BY vendor ORDER BY count DESC", when, "Unknown")
-
-               return ((CPU_VENDORS.get(r.vendor, r.vendor), r.count) for r in res)
-
-       def get_cpu_clock_speeds(self, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT AVG(fireinfo_profiles_processors.clock_speed) AS avg, \
-                       STDDEV(fireinfo_profiles_processors.clock_speed) AS stddev, \
-                       MIN(fireinfo_profiles_processors.clock_speed) AS min, \
-                       MAX(fireinfo_profiles_processors.clock_speed) AS max FROM profiles \
-                       LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
-                       WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
-                       AND fireinfo_profiles_processors.clock_speed > 0 \
-                       AND fireinfo_profiles_processors.clock_speed < fireinfo_profiles_processors.bogomips \
-                       AND fireinfo_profiles_processors.bogomips <= %s", when, 10000)
+               if when:
+                       raise NotImplementedError
 
-               if res:
-                       return (res.avg or 0, res.stddev or 0, res.min or 0, res.max or 0)
-
-       def get_cpus_with_platform_and_flag(self, platform, flag, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
-                       processors AS (SELECT fireinfo_processors.id AS id, fireinfo_processors.flags AS flags FROM profiles \
-                       LEFT JOIN fireinfo_profiles_processors ON profiles.id = fireinfo_profiles_processors.profile_id \
-                       LEFT JOIN fireinfo_processors ON fireinfo_profiles_processors.processor_id = fireinfo_processors.id \
-                       LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
-                       LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
-                       WHERE NOT fireinfo_profiles_processors.processor_id IS NULL \
-                       AND fireinfo_arches.platform = %s AND NOT 'hypervisor' = ANY(fireinfo_processors.flags)) \
-                       SELECT (COUNT(*)::float / (SELECT NULLIF(COUNT(*), 0) FROM processors)) AS count FROM processors \
-                       WHERE %s = ANY(processors.flags)", when, platform, flag)
-
-               return res.count or 0
-
-       def get_common_cpu_flags_by_platform(self, platform, when=None):
-               if platform == "arm":
-                       flags = (
-                               "lpae", "neon", "thumb", "thumbee", "vfpv3", "vfpv4",
-                       )
-               elif platform == "x86":
-                       flags = (
-                               "aes", "avx", "avx2", "lm", "mmx", "mmxext", "nx", "pae",
-                               "pni", "popcnt", "sse", "sse2", "rdrand", "ssse3", "sse4a",
-                               "sse4_1", "sse4_2", "pclmulqdq", "rdseed",
-                       )
                else:
-                       return
+                       res = self.db.query("""
+                               SELECT
+                                       blob->'cpu'->'vendor' AS vendor,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       blob IS NOT NULL
+                               AND
+                                       blob->'cpu'->'vendor' IS NOT NULL
+                               GROUP BY
+                                       blob->'cpu'->'vendor'
+                       """)
+
+               return { CPU_VENDORS.get(row.vendor, row.vendor) : row.p for row in res }
+
+       def get_cpu_flags_map(self, when=None):
+               if when:
+                       raise NotImplementedError
 
-               ret = []
-               for flag in flags:
-                       ret.append((flag, self.get_cpus_with_platform_and_flag(platform, flag, when=when)))
+               else:
+                       res = self.db.query("""
+                               WITH arch_flags AS (
+                                       SELECT
+                                               ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
+                                               blob->'cpu'->'arch' AS arch,
+                                               blob->'cpu'->'flags' AS flags
+                                       FROM
+                                               fireinfo
+                                       WHERE
+                                               expired_at IS NULL
+                                       AND
+                                               blob->'cpu'->'arch' IS NOT NULL
+                                       AND
+                                               blob->'cpu'->'flags' IS NOT NULL
+
+                                       -- Filter out virtual systems
+                                       AND
+                                               CAST((blob->'system'->'virtual') AS boolean) IS FALSE
+                               )
+
+                               SELECT
+                                       arch,
+                                       flag,
+                                       fireinfo_percentage(
+                                               COUNT(*),
+                                               (
+                                                       SELECT
+                                                               MAX(id)
+                                                       FROM
+                                                               arch_flags __arch_flags
+                                                       WHERE
+                                                               arch_flags.arch = __arch_flags.arch
+                                               )
+                                       ) AS p
+                               FROM
+                                       arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
+                               GROUP BY
+                                       arch, flag
+                       """)
+
+               result = {}
 
-               # Add virtual CPU flag "virt" for virtualization support
-               if platform == "x86":
-                       ret.append(("virt",
-                               self.get_cpus_with_platform_and_flag(platform, "vmx", when=when) + \
-                               self.get_cpus_with_platform_and_flag(platform, "svm", when=when)))
+               for row in res:
+                       try:
+                               result[row.arch][row.flag] = row.p
+                       except KeyError:
+                               result[row.arch] = { row.flag : row.p }
 
-               return sorted(ret, key=lambda x: x[1], reverse=True)
+               return result
 
        def get_average_memory_amount(self, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                               SELECT AVG(fireinfo_profiles_memory.amount) AS avg FROM profiles \
-                               LEFT JOIN fireinfo_profiles_memory ON profiles.id = fireinfo_profiles_memory.profile_id", when)
-
-               if res:
-                               return res.avg or 0
+               if when:
+                       res = self.db.get("""
+                               SELECT
+                                       AVG(
+                                               CAST(blob->'system'->'memory' AS numeric)
+                                       ) AS memory
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       created_at <= %s
+                               AND
+                                       (
+                                               expired_at IS NULL
+                                       OR
+                                               expired_at > %s
+                                       )
+                       """, when)
+               else:
+                       res = self.db.get("""
+                               SELECT
+                                       AVG(
+                                               CAST(blob->'system'->'memory' AS numeric)
+                                       ) AS memory
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                       """,)
+
+               return res.memory if res else 0
 
        def get_arch_map(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT fireinfo_arches.name AS arch, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count \
-                       FROM profiles \
-                       LEFT JOIN fireinfo_profiles_arches ON profiles.id = fireinfo_profiles_arches.profile_id \
-                       LEFT JOIN fireinfo_arches ON fireinfo_profiles_arches.arch_id = fireinfo_arches.id \
-                       WHERE NOT fireinfo_profiles_arches.profile_id IS NULL \
-                       GROUP BY fireinfo_arches.id ORDER BY count DESC", when)
+               if when:
+                       raise NotImplementedError
 
-               return ((r.arch, r.count) for r in res)
+               else:
+                       res = self.db.query("""
+                               SELECT
+                                       blob->'cpu'->'arch' AS arch,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       blob->'cpu'->'arch' IS NOT NULL
+                               GROUP BY
+                                       blob->'cpu'->'arch'
+                       """)
+
+               return { row.arch : row.p for row in res }
 
        # Virtual
 
        def get_hypervisor_map(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
-                       virtual_profiles AS (SELECT profiles.id AS profile_id, fireinfo_profiles_virtual.hypervisor_id FROM profiles \
-                               LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
-                               WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL) \
-                       SELECT COALESCE(fireinfo_hypervisors.name, %s) AS name, \
-                               COUNT(*)::float / (SELECT COUNT(*) FROM virtual_profiles) AS count FROM virtual_profiles \
-                       LEFT JOIN fireinfo_hypervisors ON virtual_profiles.hypervisor_id = fireinfo_hypervisors.id \
-                       GROUP BY fireinfo_hypervisors.name ORDER BY count DESC", when, "unknown")
-
-               return ((r.name, r.count) for r in res)
+               if when:
+                       raise NotImplementedError
+               else:
+                       res = self.db.query("""
+                               SELECT
+                                       blob->'hypervisor'->'vendor' AS vendor,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+                               AND
+                                       blob->'hypervisor'->'vendor' IS NOT NULL
+                               GROUP BY
+                                       blob->'hypervisor'->'vendor'
+                       """)
+
+               return { row.vendor : row.p for row in res }
 
        def get_virtual_ratio(self, when=None):
-               res = self.db.get("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_virtual ON profiles.id = fireinfo_profiles_virtual.profile_id \
-                       WHERE fireinfo_profiles_virtual.profile_id IS NOT NULL", when)
+               if when:
+                       raise NotImplementedError
 
-               if res:
-                       return res.count
+               else:
+                       res = self.db.get("""
+                               SELECT
+                                       fireinfo_percentage(
+                                               COUNT(*) FILTER (
+                                                       WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
+                                               ),
+                                               COUNT(*)
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       blob IS NOT NULL
+                       """)
+
+               return res.p if res else 0
 
        # Releases
 
        def get_releases_map(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT fireinfo_releases.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_releases ON profiles.id = fireinfo_profiles_releases.profile_id \
-                       LEFT JOIN fireinfo_releases ON fireinfo_profiles_releases.release_id = fireinfo_releases.id \
-                       GROUP BY fireinfo_releases.name ORDER BY count DESC", when)
+               if when:
+                       raise NotImplementedError
 
-               return ((r.name, r.count) for r in res)
+               else:
+                       res = self.db.query("""
+                               SELECT
+                                       blob->'system'->'release' AS release,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       blob IS NOT NULL
+                               AND
+                                       blob->'system'->'release' IS NOT NULL
+                               GROUP BY
+                                       blob->'system'->'release'
+                       """)
+
+               return { row.release : row.p for row in res }
+
+       # Kernels
 
        def get_kernels_map(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT fireinfo_kernels.name, COUNT(*)::float / (SELECT COUNT(*) FROM profiles) AS count FROM profiles \
-                       LEFT JOIN fireinfo_profiles_kernels ON profiles.id = fireinfo_profiles_kernels.profile_id \
-                       LEFT JOIN fireinfo_kernels ON fireinfo_profiles_kernels.kernel_id = fireinfo_kernels.id \
-                       GROUP BY fireinfo_kernels.name ORDER BY count DESC", when)
-
-               return ((r.name, r.count) for r in res)
-
-       def _process_devices(self, devices):
-               result = []
-
-               for dev in devices:
-                       dev = Device(self.backend, dev.get("id", None), dev)
-                       result.append(dev)
-
-               return result
-
-       def get_driver_map(self, driver, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
-                       devices AS (SELECT * FROM profiles \
-                               LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
-                               LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
-                               WHERE driver = %s) \
-                       SELECT subsystem, model, vendor, driver, deviceclass, \
-                               COUNT(*)::float / (SELECT COUNT(*) FROM devices) AS percentage FROM devices \
-                               GROUP BY subsystem, model, vendor, driver, deviceclass \
-                               ORDER BY percentage DESC", when, driver)
+               if when:
+                       raise NotImplementedError
 
-               return self._process_devices(res)
+               else:
+                       res = self.db.query("""
+                               SELECT
+                                       blob->'system'->'kernel' AS kernel,
+                                       fireinfo_percentage(
+                                               COUNT(*), SUM(COUNT(*)) OVER ()
+                                       ) AS p
+                               FROM
+                                       fireinfo
+                               WHERE
+                                       expired_at IS NULL
+                               AND
+                                       blob IS NOT NULL
+                               AND
+                                       blob->'system'->'kernel' IS NOT NULL
+                               GROUP BY
+                                       blob->'system'->'kernel'
+                       """)
+
+               return { row.kernel : row.p for row in res }
 
        subsystem2class = {
                "pci" : hwdata.PCI(),
@@ -1980,15 +1286,45 @@ class Fireinfo(Object):
                return cls.get_device(vendor_id, model_id) or ""
 
        def get_vendor_list(self, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id) \
-                       SELECT DISTINCT fireinfo_devices.subsystem AS subsystem, fireinfo_devices.vendor AS vendor FROM profiles \
-                               LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
-                               LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
-                               WHERE NOT fireinfo_devices.driver = ANY(%s)", when, IGNORED_DEVICES)
+               if when:
+                       raise NotImplementedError
+
+               else:
+                       res = self.db.query("""
+                               WITH devices AS (
+                                       SELECT
+                                               jsonb_array_elements(blob->'devices') AS device
+                                       FROM
+                                               fireinfo
+                                       WHERE
+                                               expired_at IS NULL
+                                       AND
+                                               blob IS NOT NULL
+                                       AND
+                                               blob->'devices' IS NOT NULL
+                                       AND
+                                               jsonb_typeof(blob->'devices') = 'array'
+                               )
+
+                               SELECT
+                                       devices.device->'subsystem' AS subsystem,
+                                       devices.device->'vendor' AS vendor
+                               FROM
+                                       devices
+                               WHERE
+                                       devices.device->'subsystem' IS NOT NULL
+                               AND
+                                       devices.device->'vendor' IS NOT NULL
+                               AND
+                                       NOT devices.device->>'driver' = 'usb'
+                               GROUP BY
+                                       subsystem, vendor
+                       """)
 
                vendors = {}
+
                for row in res:
-                       vendor = self.get_vendor_string(row.subsystem, row.vendor)
+                       vendor = self.get_vendor_string(row.subsystem, row.vendor) or row.vendor
 
                        # Drop if vendor could not be determined
                        if vendor is None:
@@ -1999,17 +1335,111 @@ class Fireinfo(Object):
                        except KeyError:
                                vendors[vendor] = [(row.subsystem, row.vendor)]
 
-               vendors = list(vendors.items())
-               return sorted(vendors)
+               return vendors
+
+       def _get_devices(self, query, *args, **kwargs):
+               res = self.db.query(query, *args, **kwargs)
+
+               return [Device(self.backend, blob) for blob in res]
 
        def get_devices_by_vendor(self, subsystem, vendor, when=None):
-               res = self.db.query("WITH profiles AS (SELECT fireinfo_profiles_with_data_at(%s) AS id), \
-                       devices AS (SELECT * FROM profiles \
-                               LEFT JOIN fireinfo_profiles_devices ON profiles.id = fireinfo_profiles_devices.profile_id \
-                               LEFT JOIN fireinfo_devices ON fireinfo_profiles_devices.device_id = fireinfo_devices.id \
-                               WHERE NOT fireinfo_devices.driver = ANY(%s)), \
-                       vendor_devices AS (SELECT * FROM devices WHERE devices.subsystem = %s AND devices.vendor = %s) \
-                       SELECT subsystem, model, vendor, driver, deviceclass FROM vendor_devices \
-                               GROUP BY subsystem, model, vendor, driver, deviceclass", when, IGNORED_DEVICES, subsystem, vendor)
-
-               return self._process_devices(res)
+               if when:
+                       raise NotImplementedError
+
+               else:
+                       return self._get_devices("""
+                               WITH devices AS (
+                                       SELECT
+                                               jsonb_array_elements(blob->'devices') AS device
+                                       FROM
+                                               fireinfo
+                                       WHERE
+                                               expired_at IS NULL
+                                       AND
+                                               blob IS NOT NULL
+                                       AND
+                                               blob->'devices' IS NOT NULL
+                                       AND
+                                               jsonb_typeof(blob->'devices') = 'array'
+                               )
+
+                               SELECT
+                                       device.deviceclass,
+                                       device.subsystem,
+                                       device.vendor,
+                                       device.model,
+                                       device.driver
+                               FROM
+                                       devices,
+                                       jsonb_to_record(devices.device) AS device(
+                                               deviceclass text,
+                                               subsystem   text,
+                                               vendor      text,
+                                               sub_vendor  text,
+                                               model       text,
+                                               sub_model   text,
+                                               driver      text
+                                       )
+                               WHERE
+                                       devices.device->>'subsystem' = %s
+                               AND
+                                       devices.device->>'vendor' = %s
+                               AND
+                                       NOT devices.device->>'driver' = 'usb'
+                               GROUP BY
+                                       device.deviceclass,
+                                       device.subsystem,
+                                       device.vendor,
+                                       device.model,
+                                       device.driver
+                               """, subsystem, vendor,
+                       )
+
+       def get_devices_by_driver(self, driver, when=None):
+               if when:
+                       raise NotImplementedError
+
+               else:
+                       return self._get_devices("""
+                               WITH devices AS (
+                                       SELECT
+                                               jsonb_array_elements(blob->'devices') AS device
+                                       FROM
+                                               fireinfo
+                                       WHERE
+                                               expired_at IS NULL
+                                       AND
+                                               blob IS NOT NULL
+                                       AND
+                                               blob->'devices' IS NOT NULL
+                                       AND
+                                               jsonb_typeof(blob->'devices') = 'array'
+                               )
+
+                               SELECT
+                                       device.deviceclass,
+                                       device.subsystem,
+                                       device.vendor,
+                                       device.model,
+                                       device.driver
+                               FROM
+                                       devices,
+                                       jsonb_to_record(devices.device) AS device(
+                                               deviceclass text,
+                                               subsystem   text,
+                                               vendor      text,
+                                               sub_vendor  text,
+                                               model       text,
+                                               sub_model   text,
+                                               driver      text
+                                       )
+                               WHERE
+                                       devices.device->>'driver' = '%s'
+                               GROUP BY
+                                       device.deviceclass,
+                                       device.subsystem,
+                                       device.vendor,
+                                       device.model,
+                                       device.driver
+                               """, driver,
+                       )
diff --git a/src/backend/memcached.py b/src/backend/memcached.py
deleted file mode 100644 (file)
index 56a8cc8..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/python
-
-import logging
-import memcache
-
-from .misc import Object
-
-class Memcached(Object):
-       def init(self):
-               self._connection = memcache.Client(["localhost"], debug=1)
-
-       def get(self, key, *args, **kwargs):
-               key = self._sanitize_key(key)
-
-               logging.debug("Retrieving %s from cache..." % key)
-
-               ret = self._connection.get(key, *args, **kwargs)
-
-               if ret is None:
-                       logging.debug("Found nothing for %s" % key)
-               else:
-                       logging.debug("Found object for %s" % key)
-
-               return ret
-
-       def get_multi(self, keys, *args, **kwargs):
-               keys = (self._sanitize_key(key) for key in keys)
-
-               logging.debug("Retrieving keys from cache: %s" % keys)
-
-               ret = self._connection.get_multi(keys, *args, **kwargs)
-
-               if ret is None:
-                       logging.debug("Found nothing for %s" % keys)
-               else:
-                       logging.debug("Found objects for %s" % keys)
-
-               return ret
-
-       def add(self, key, data, *args, **kwargs):
-               key = self._sanitize_key(key)
-
-               if data is None:
-                       logging.debug("Putting nothing into cache for %s" % key)
-               else:
-                       logging.debug("Putting object into cache for %s" % key)
-
-               return self._connection.add(key, data, *args, **kwargs)
-
-       def set(self, key, data, *args, **kwargs):
-               key = self._sanitize_key(key)
-
-               if data is None:
-                       logging.debug("Putting nothing into cache for %s" % key)
-               else:
-                       logging.debug("Putting object into cache for %s" % key)
-
-               return self._connection.set(key, data, *args, **kwargs)
-
-       def delete(self, key, *args, **kwargs):
-               key = self._sanitize_key(key)
-
-               return self._connection.delete(key, *args, **kwargs)
-
-       def incr(self, key):
-               key = self._sanitize_key(key)
-
-               logging.debug("Incrementing key %s" % key)
-
-               return self._connection.incr(key)
-
-       @staticmethod
-       def _sanitize_key(key):
-               # Memcache does not seem to like any spaces
-               return key.replace(" ", "-")
index 817a6f10d4eccbe6a77940c47948526f19d5b51d..53f62baa33bda857dc0beaaee4f1aca3c36ea954 100644 (file)
@@ -1,10 +1,14 @@
 #!/usr/bin/python3
 
+import base64
 import email
 import email.mime.multipart
 import email.mime.text
 import email.utils
 import logging
+import mimetypes
+import os.path
+import pynliner
 import random
 import smtplib
 import socket
@@ -17,6 +21,9 @@ from . import misc
 from . import util
 from .decorators import *
 
+# Encode emails in UTF-8 by default
+email.charset.add_charset("utf-8", email.charset.SHORTEST, email.charset.QP, "utf-8")
+
 class Messages(misc.Object):
        @lazy_property
        def queue(self):
@@ -30,7 +37,12 @@ class Messages(misc.Object):
                templates_dir = self.backend.config.get("global", "templates_dir")
                assert templates_dir
 
-               return tornado.template.Loader(templates_dir, autoescape=None)
+               # Setup namespace
+               namespace = {
+                       "embed_image" : self.embed_image,
+               }
+
+               return tornado.template.Loader(templates_dir, namespace=namespace, autoescape=None)
 
        def make_recipient(self, recipient):
                # Use the contact instead of the account
@@ -137,6 +149,10 @@ class Messages(misc.Object):
                                except KeyError:
                                        message.add_header(header, value)
 
+                       # Inline any CSS
+                       if extension == "html":
+                               message_part = self._inline_css(message_part)
+
                        # Create a MIMEText object out of it
                        message_part = email.mime.text.MIMEText(
                                message_part.get_payload(), mimetype)
@@ -153,6 +169,44 @@ class Messages(misc.Object):
                if self.backend.debug:
                        self.template_loader.reset()
 
+       def _inline_css(self, part):
+               """
+                       Inlines any CSS into style attributes
+               """
+               # Fetch the payload
+               payload = part.get_payload()
+
+               # Setup Pynliner
+               p = pynliner.Pynliner().from_string(payload)
+
+               # Run the inlining
+               payload = p.run()
+
+               # Set the payload again
+               part.set_payload(payload)
+
+               return part
+
+       def embed_image(self, path):
+               static_dir = self.backend.config.get("global", "static_dir")
+               assert static_dir
+
+               # Make the path absolute
+               path = os.path.join(static_dir, path)
+
+               # Fetch the mimetype
+               mimetype, encoding = mimetypes.guess_type(path)
+
+               # Read the file
+               with open(path, "rb") as f:
+                       data = f.read()
+
+               # Convert data into base64
+               data = base64.b64encode(data)
+
+               # Return everything
+               return "data:%s;base64,%s" % (mimetype, data.decode())
+
        async def send_cli(self, template, recipient):
                """
                        Send a test message from the CLI
index 4960e3070b7bd26cea93310a99fad76f2112de82..12474435309f01f56336c185e30e259229f0b176 100644 (file)
@@ -34,10 +34,6 @@ class Object(object):
        def iuse(self):
                return self.backend.iuse
 
-       @property
-       def memcache(self):
-               return self.backend.memcache
-
        @property
        def settings(self):
                return self.backend.settings
index ec99cc511ac7d4068efa42c406002a4050c0c57b..f4b2b4c129ec80aed31a1982097a5b65dd59ad20 100644 (file)
@@ -11,8 +11,6 @@ class RateLimiter(misc.Object):
 
 
 class RateLimiterRequest(misc.Object):
-       prefix = "ratelimit"
-
        def init(self, request, handler, minutes, limit):
                self.request = request
                self.handler = handler
@@ -21,81 +19,80 @@ class RateLimiterRequest(misc.Object):
                self.minutes = minutes
                self.limit   = limit
 
+               # What is the current time?
                self.now = datetime.datetime.utcnow()
 
-               # Fetch the current counter value from the cache
-               self.counter = self.get_counter()
-
-               # Increment the rate-limiting counter
-               self.increment_counter()
+               # When to expire?
+               self.expires_at = self.now + datetime.timedelta(minutes=self.minutes + 1)
 
-               # Write the header if we are not limited
-               if not self.is_ratelimited():
-                       self.write_headers()
+               self.prefix = "-".join((
+                       self.__class__.__name__,
+                       self.request.host,
+                       self.request.path,
+                       self.request.method,
+                       self.request.remote_ip,
+               ))
 
-       def is_ratelimited(self):
+       async def is_ratelimited(self):
                """
                        Returns True if the request is prohibited by the rate limiter
                """
+               counter = await self.get_counter()
+
                # The client is rate-limited when more requests have been
                # received than allowed.
-               return self.counter >= self.limit
+               if counter >= self.limit:
+                       return True
+
+               # Increment the counter
+               await self.increment_counter()
+
+               # If not ratelimited, write some headers
+               self.write_headers(counter=counter)
+
+       @property
+       def key(self):
+               return "%s-%s" % (self.prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+
+       @property
+       def keys_to_check(self):
+               for minute in range(self.minutes + 1):
+                       when = self.now - datetime.timedelta(minutes=minute)
+
+                       yield "%s-%s" % (self.prefix, when.strftime("%Y-%m-%d-%H:%M"))
 
-       def get_counter(self):
+       async def get_counter(self):
                """
                        Returns the number of requests that have been done in
                        recent time.
                """
-               keys = self.get_keys_to_check()
+               async with await self.backend.cache.pipeline() as p:
+                       for key in self.keys_to_check:
+                               await p.get(key)
 
-               res = self.memcache.get_multi(keys)
-               if res:
-                       return sum((int(e) for e in res.values()))
+                       # Run the pipeline
+                       res = await p.execute()
 
-               return 0
+               # Return the sum
+               return sum((int(e) for e in res if e))
 
-       def write_headers(self):
+       def write_headers(self, counter):
                # Send the limit to the user
                self.handler.set_header("X-Rate-Limit-Limit", self.limit)
 
                # Send the user how many requests are left for this time window
-               self.handler.set_header("X-Rate-Limit-Remaining",
-                       self.limit - self.counter)
+               self.handler.set_header("X-Rate-Limit-Remaining", self.limit - counter)
 
-               expires = self.now + datetime.timedelta(seconds=self.expires_after)
-               self.handler.set_header("X-Rate-Limit-Reset", expires.strftime("%s"))
+               # Send when the limit resets
+               self.handler.set_header("X-Rate-Limit-Reset", self.expires_at.strftime("%s"))
 
-       def get_key(self):
-               key_prefix = self.get_key_prefix()
+       async def increment_counter(self):
+               async with await self.backend.cache.pipeline() as p:
+                       # Increment the key
+                       await p.incr(self.key)
 
-               return "%s-%s" % (key_prefix, self.now.strftime("%Y-%m-%d-%H:%M"))
+                       # Set expiry
+                       await p.expireat(self.key, self.expires_at)
 
-       def get_keys_to_check(self):
-               key_prefix = self.get_key_prefix()
-
-               keys = []
-               for minute in range(self.minutes + 1):
-                       when = self.now - datetime.timedelta(minutes=minute)
-
-                       key = "%s-%s" % (key_prefix, when.strftime("%Y-%m-%d-%H:%M"))
-                       keys.append(key)
-
-               return keys
-
-       def get_key_prefix(self):
-               return "-".join((self.prefix, self.request.host, self.request.path,
-                       self.request.method, self.request.remote_ip,))
-
-       def increment_counter(self):
-               key = self.get_key()
-
-               # Add the key or increment if it already exists
-               if not self.memcache.add(key, "1", self.expires_after):
-                       self.memcache.incr(key)
-
-       @property
-       def expires_after(self):
-               """
-                       Returns the number of seconds after which the counter has reset.
-               """
-               return (self.minutes + 1) * 60
+                       # Run the pipeline
+                       await p.execute()
index e399613ef640d19213e2fc27d069e8253d6d007a..bed051d04ce477ecca4e5133fde0a48bb38ca5c2 100644 (file)
@@ -250,16 +250,6 @@ class Release(Object):
                if self.__data.blog_id:
                        return self.backend.blog.get_by_id(self.__data.blog_id)
 
-       @property
-       def fireinfo_id(self):
-               name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
-
-               res = self.db.get("SELECT id FROM fireinfo_releases \
-                       WHERE name = %s", name)
-
-               if res:
-                       return res.id
-
        @property
        def stable(self):
                return self.__data.stable
@@ -371,10 +361,13 @@ class Release(Object):
 
        # Fireinfo Stuff
 
-       @property
-       def penetration(self):
+       def get_usage(self, when=None):
+               name = self.sname.replace("ipfire-", "IPFire ").replace("-", " - ")
+
                # Get penetration from fireinfo
-               return self.backend.fireinfo.get_release_penetration(self)
+               releases = self.backend.fireinfo.get_releases_map(when=when)
+
+               return releases.get(name, 0)
 
 
 class Releases(Object):
index ef83e1e364279b1cc9a895b3b4c81e72ebc0be31..204544cb7eb946f4c5a1a69d3b9990ebe39481b6 100644 (file)
@@ -55,22 +55,12 @@ class Wiki(misc.Object):
                return page and not page.was_deleted()
 
        def get_page_title(self, page, default=None):
-               # Try to retrieve title from cache
-               title = self.memcache.get("wiki:title:%s" % page)
-               if title:
-                       return title
-
-               # If the title has not been in the cache, we will
-               # have to look it up
                doc = self.get_page(page)
                if doc:
                        title = doc.title
                else:
                        title = os.path.basename(page)
 
-               # Save in cache for forever
-               self.memcache.set("wiki:title:%s" % page, title)
-
                return title
 
        def get_page(self, page, revision=None):
@@ -111,9 +101,6 @@ class Wiki(misc.Object):
                page = self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
                        VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
 
-               # Update cache
-               self.memcache.set("wiki:title:%s" % page.page, page.title)
-
                # Send email to all watchers
                page._send_watcher_emails(excludes=[author])
 
@@ -561,13 +548,18 @@ class File(misc.Object):
                if res:
                        return bytes(res.data)
 
-       def get_thumbnail(self, size):
+       async def get_thumbnail(self, size):
                assert self.is_bitmap_image()
 
-               cache_key = "-".join((self.path, util.normalize(self.filename), self.created_at.isoformat(), "%spx" % size))
+               cache_key = "-".join((
+                       self.path,
+                       util.normalize(self.filename),
+                       self.created_at.isoformat(),
+                       "%spx" % size,
+               ))
 
                # Try to fetch the data from the cache
-               thumbnail = self.memcache.get(cache_key)
+               thumbnail = await self.backend.cache.get(cache_key)
                if thumbnail:
                        return thumbnail
 
@@ -575,7 +567,7 @@ class File(misc.Object):
                thumbnail = util.generate_thumbnail(self.blob, size)
 
                # Put it into the cache for forever
-               self.memcache.set(cache_key, thumbnail)
+               await self.backend.cache.set(cache_key, thumbnail)
 
                return thumbnail
 
index c999a0ca093ee36f9b7d9c6e23c5b29e72d75322..c156a0a333abd1060f173a0876a63b154bce388e 100644 (file)
@@ -60,6 +60,8 @@ class ZeiterfassungClient(Object):
                logging.debug("Sending request to %s:" % request.url)
                for header in sorted(request.headers):
                        logging.debug(" %s: %s" % (header, request.headers[header]))
+               if request.body:
+                       logging.debug("%s" % json.dumps(kwargs, indent=4, sort_keys=True))
 
                # Send the request
                response = await self.backend.http_client.fetch(request)
@@ -72,15 +74,21 @@ class ZeiterfassungClient(Object):
 
                # Fetch the whole body
                body = response.body
+               if body:
+                       # Decode the JSON response
+                       body = json.loads(body)
+
+                       # Log what we have received in a human-readable way
+                       logging.debug("%s" % json.dumps(body, indent=4, sort_keys=True))
 
                # Fetch the signature
                signature = response.headers.get("Hash")
                if not signature:
                        raise RuntimeError("Could not find signature on response")
 
-               expected_signature = self._sign_response(body)
+               expected_signature = self._sign_response(response.body)
                if not hmac.compare_digest(expected_signature, signature):
                        raise RuntimeError("Invalid signature: %s" % signature)
 
-               # Decode the JSON response
-               return json.loads(body)
+               # Return the body
+               return body
index d2a18d2199bf58a9264dbcf3467ea19d5efedf96..47ca754c25f8498d55bf51b36a3e52f110a7b8c6 100644 (file)
@@ -1,9 +1,11 @@
+$baseurl: !default
+
 /* latin-ext */
 @font-face
        font-family: "Prompt"
        font-style: normal
        font-weight: 400
-       src: local("Prompt Regular"), local("Prompt-Regular"), url(/static/fonts/Prompt-Regular.ttf) format("truetype")
+       src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$baseurl}/static/fonts/Prompt-Regular.ttf) format("truetype")
        unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
 
 /* latin */
@@ -11,7 +13,7 @@
        font-family: "Prompt"
        font-style: normal
        font-weight: 400
-       src: local("Prompt Regular"), local("Prompt-Regular"), url(/static/fonts/Prompt-Regular.ttf) format("truetype")
+       src: local("Prompt Regular"), local("Prompt-Regular"), url(#{$baseurl}/static/fonts/Prompt-Regular.ttf) format("truetype")
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
 
 /* latin-ext */
@@ -19,7 +21,7 @@
        font-family: 'Prompt'
        font-style: normal
        font-weight: 500
-       src: local('Prompt Medium'), local('Prompt-Medium'), url(/static/fonts/Prompt-Medium.ttf) format("truetype")
+       src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$baseurl}/static/fonts/Prompt-Medium.ttf) format("truetype")
        unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
 
 /* latin */
@@ -27,7 +29,7 @@
        font-family: 'Prompt'
        font-style: normal
        font-weight: 500
-       src: local('Prompt Medium'), local('Prompt-Medium'), url(/static/fonts/Prompt-Medium.ttf) format("truetype")
+       src: local('Prompt Medium'), local('Prompt-Medium'), url(#{$baseurl}/static/fonts/Prompt-Medium.ttf) format("truetype")
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
 
 /* latin-ext */
@@ -35,7 +37,7 @@
        font-family: "Prompt"
        font-style: normal
        font-weight: 700
-       src: local("Prompt Bold"), local("Prompt-Bold"), url(/static/fonts/Prompt-Bold.ttf) format("truetype")
+       src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$baseurl}/static/fonts/Prompt-Bold.ttf) format("truetype")
        unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF
 
 /* latin */
@@ -43,5 +45,5 @@
        font-family: "Prompt"
        font-style: normal
        font-weight: 700
-       src: local("Prompt Bold"), local("Prompt-Bold"), url(/static/fonts/Prompt-Bold.ttf) format("truetype")
+       src: local("Prompt Bold"), local("Prompt-Bold"), url(#{$baseurl}/static/fonts/Prompt-Bold.ttf) format("truetype")
        unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD
index 103449fda618e3bda7f56421e83ae0fb38b206f7..67ec60ba8e7947fd5d7cb768c7b2031915b2aa7f 100644 (file)
@@ -1 +1,23 @@
-// To be re-done https://bugzilla.ipfire.org/show_bug.cgi?id=13051
+@import "main.sass"
+
+// Make the body stretch over the entire screen
+body
+       @extend .container
+
+       // Add some space around the content
+       padding: 3rem 1rem;
+
+h1
+       @extend .title, .is-3
+
+// Make all tables .table by default
+table
+    @extend .table, .is-fullwidth, .is-bordered, .is-striped, .is-hoverable
+
+// Fix to show the bottom line of the table
+table
+       tr
+               &:last-child
+                       td,
+                       th
+                               border-bottom-width: 1px !important
index 43fe50e2070c7f7f697a67b798725b3434d59e94..746724f638b3c4ba3a99269c738a6f3d224486f3 100644 (file)
@@ -6,6 +6,17 @@
 // Global Settings
 $family-sans-serif:                            Prompt, sans-serif
 
+$size-1:                                               3rem
+$size-2:                                               2.5rem
+$size-3:                                               2rem
+$size-4:                                               1.5rem
+$size-5:                                               1.25rem
+$size-6:                                               1rem
+$size-7:                                               0.75rem
+
+// Make titles slightly larger
+$title-size:                                   $size-2
+
 // Colour Palette
 $primary:                                              #ff2e52
 $primary-invert:                               #ffffff
@@ -16,10 +27,18 @@ $success-invert:                #ffffff
 $danger:                                               #ac001a
 $warning:                                              #f3ff50
 
+// Pride Colours
+$pride-red:                                            #e40303
+$pride-orange:                                 #ff8c00
+$pride-yellow:                                 #ffed00
+$pride-green:                                  #008026
+$pride-blue:                                   #24408e
+$pride-purple:                                 #732982
+
 // Custom Colours
-$lwl:                                                   #6534C8
+$lwl:                                                  #6534C8
 
-$custom-colors: ("secondary" : ($secondary, $secondary-invert), "lwl" : ($lwl, $white))
+$custom-colors: ("secondary" : ($secondary, $secondary-invert), "lwl" : ($lwl, $white), "pride-red" : ($pride-red, $white), "pride-orange" : ($pride-orange, $black), "pride-yellow" : ($pride-yellow, $black), "pride-green" : ($pride-green, $white), "pride-blue" : ($pride-blue, $white), "pride-purple" : ($pride-purple, $white))
 
 // Use the primary colour for links
 $link:                                                 $primary
@@ -47,6 +66,9 @@ $breadcrumb-item-active-color:  $primary
 $section-padding:               3rem 1.5rem
 $section-padding-desktop:       3rem 0.5rem
 
+// Footer
+$footer-padding:                               3rem 1.5rem 3rem
+
 // Import Bulma
 @import "../third-party/bulma/sass/utilities/_all.sass"
 @import "../third-party/bulma/sass/base/_all.sass"
index f74e0816c07624d60cafdcd786772808fc922898..fa81706126b8b0ae18a023c72e1d9894bafc9464 100644 (file)
@@ -1,32 +1,13 @@
 #!@PYTHON@
 
+import asyncio
 import sys
-import tornado.ioloop
 import tornado.options
 
 import ipfire
 
-class TaskRunner(object):
-       def __init__(self, *args, **kwargs):
-               self.backend = ipfire.Backend(*args, **kwargs)
-
-               # Create an IOLoop
-               self.ioloop = tornado.ioloop.IOLoop.current()
-
-       def run_task(self, name, *args, **kwargs):
-               """
-                       This method runs the task with the given name and
-                       arguments asynchronically and exits the program in
-                       case on a non-zero exit code
-               """
-               async def task():
-                       await self.backend.run_task(name, *args, **kwargs)
-
-               return self.ioloop.run_sync(task)
-
-
-def main():
-       z = TaskRunner("@configsdir@/@PACKAGE_NAME@.conf")
+async def main():
+       backend = ipfire.Backend("@configsdir@/@PACKAGE_NAME@.conf")
 
        if len(sys.argv) < 2:
                sys.stderr.write("Argument needed\n")
@@ -36,6 +17,6 @@ def main():
        args = tornado.options.parse_command_line()
 
        # Run the task
-       z.run_task(*args)
+       await backend.run_task(*args)
 
-main()
+asyncio.run(main())
diff --git a/src/static/img/auth/register.jpg b/src/static/img/auth/register.jpg
new file mode 100644 (file)
index 0000000..d422069
Binary files /dev/null and b/src/static/img/auth/register.jpg differ
index 0ffdcbd7fc8b4fb01212728d3583be3b7cebf3d4..19f7e8b596c25213a2c8642283b567b158b9895d 100644 (file)
@@ -3,50 +3,49 @@
 {% block title %}{{ _("Log In") }}{% end block %}
 
 {% block container %}
-       <section class="section">
-               <div class="container">
-                       <div class="columns is-centered">
-                               <div class="column is-one-third">
-                                       <h1 class="title is-1">
-                                               IPFire<span class="has-text-primary">_</span>
-                                       </h1>
-                                       <h4 class="subtitle is-4">{{ _("Log In") }}</h4>
-
-                                       <div class="block">
-                                               <form action="" method="POST">
-                                                       {% raw xsrf_form_html() %}
-
-                                                       {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
-
-                                                       <div class="field">
-                                                               <div class="control">
-                                                                       <input class="input is-medium {% if incorrect %}is-danger{% end %}"
-                                                                               type="text" name="username" {% if username %}value="{{ username }}"{% end %}
-                                                                               placeholder="{{ _("Username") }}" required autofocus>
+       <section class="hero is-primary is-fullheight-with-navbar">
+               <div class="hero-body">
+                       <div class="container">
+                               <div class="columns is-centered">
+                                       <div class="column is-one-third">
+                                               <h1 class="title">{{ _("Log In") }}</h1>
+
+                                               <div class="block">
+                                                       <form action="" method="POST">
+                                                               {% raw xsrf_form_html() %}
+
+                                                               {% if next %}<input type="hidden" name="next" value="{{ next }}">{% end %}
+
+                                                               <div class="field">
+                                                                       <div class="control">
+                                                                               <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+                                                                                       type="text" name="username" {% if username %}value="{{ username }}"{% end %}
+                                                                                       placeholder="{{ _("Username") }}" required autofocus>
+                                                                       </div>
                                                                </div>
-                                                       </div>
 
-                                                       <div class="field">
-                                                               <div class="control">
-                                                                       <input class="input is-medium {% if incorrect %}is-danger{% end %}"
-                                                                               type="password" name="password" placeholder="{{ _("Password") }}" required>
+                                                               <div class="field">
+                                                                       <div class="control">
+                                                                               <input class="input is-medium {% if incorrect %}is-danger{% end %}"
+                                                                                       type="password" name="password" placeholder="{{ _("Password") }}" required>
+                                                                       </div>
                                                                </div>
-                                                       </div>
 
-                                                       <div class="field">
-                                                               <div class="control">
-                                                                       <button class="button is-primary is-medium is-fullwidth">
-                                                                               {{ _("Log In") }}
-                                                                       </button>
+                                                               <div class="field">
+                                                                       <div class="control">
+                                                                               <button class="button is-medium is-fullwidth">
+                                                                                       {{ _("Log In") }}
+                                                                               </button>
+                                                                       </div>
                                                                </div>
-                                                       </div>
-
-                                                       <div class="field has-text-centered">
-                                                               <a class="text-muted" href="/password-reset{% if incorrect %}?username={{ username }}{% end %}">
-                                                                       {{ _("Did you forget your password?") }}
-                                                               </a>
-                                                       </div>
-                                               </form>
+
+                                                               <div class="field has-text-centered">
+                                                                       <a class="text-muted" href="/password-reset{% if incorrect %}?username={{ username }}{% end %}">
+                                                                               {{ _("Did you forget your password?") }}
+                                                                       </a>
+                                                               </div>
+                                                       </form>
+                                               </div>
                                        </div>
                                </div>
                        </div>
index 22be8b0b9e90232c86db6cc22d26138e7288956b..480037c73f8f9719d9a1456e9d76a26f3448e539 100644 (file)
@@ -1,62 +1,56 @@
 {% extends "../../messages/base-promo.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Hey again, %s,") % account.first_name }}</strong>
-    </p>
-
-    <p>
-        {{ _("IPFire runs on supporters' donations, people like you!") }}
-    </p>
-
-    <p>
-        {{ _("Why do we need you donations?") }}
-    </p>
-
-    <ul>
-        <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
-        <li>{{ _("It helps us fund developers and extend our skills") }}</li>
-        <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
-        <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
-        <li>{{ _("It pays for our hosting") }}</li>
-    </ul>
-
-    <p>
-        {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
-    </p>
-
-    <p>
-        {{ _("If you want to see IPFire thrive, we need your support.") }}
-    </p>
-
-    <p>
-        {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
-    </p>
-
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate?frequency=monthly&amp;amount=10" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
-
-    <p>
-        {{ _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
-    </p>
-
-    <p>
-        {{ _("Thank you so much for your support,") }}
-        <br>{{ _("-Michael") }}
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hey again, %s,") % account.first_name }}</h1>
+
+                       <p>
+                               {{ _("IPFire runs on supporters' donations, people like you!") }}
+                       </p>
+
+                       <p>
+                               {{ _("Why do we need you donations?") }}
+                       </p>
+
+                       <ul>
+                               <li>{{ _("Your money ensures the longevity and long-term success of this project.") }}</li>
+                               <li>{{ _("It helps us fund developers and extend our skills") }}</li>
+                               <li>{{ _("It will aid us to promote IPFire to more people around the world") }}</li>
+                               <li>{{ _("This funds conferences, where we focus on future projects") }}</li>
+                               <li>{{ _("It pays for our hosting") }}</li>
+                       </ul>
+
+                       <p>
+                               {{ _("All this, as you would understand, requires money. Every single donation counts.") }}
+                       </p>
+
+                       <p>
+                               {{ _("If you want to see IPFire thrive, we need your support.") }}
+                       </p>
+
+                       <p>
+                               {{ _("The best way to do this is by setting up a monthly donation which you can do here:") }}
+                       </p>
+
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly&amp;amount=10">
+                                                       {{ _("Donate Now") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
+
+                       <p>
+                               {{ _("We also have other ways to donate. Please go to https://www.ipfire.org/donate for details.") }}
+                       </p>
+
+                       <p>
+                               {{ _("Thank you so much for your support,") }}
+                               <br>{{ _("-Michael") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index 8309d4ac8f9f7588958f778a5997ac134f2831d2..31e92af89e86183f7ce79b8ee6426905e25c7aa9 100644 (file)
@@ -1,29 +1,24 @@
 {% extends "../../messages/base.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Hello %s!") % account.first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+               <h1>{{ _("Hello %s!") % account.first_name }}</h1>
 
-    <p>
-        {{ _("You, or somebody else on your behalf, has requested to change your password.") }} {{ _("If this was not you, please notify a team member.") }}
-    </p>
+                       <p>
+                               {{ _("You, or somebody else on your behalf, has requested to change your password.") }}
+                               {{ _("If this was not you, please notify a team member.") }}
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://people.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}" target="_blank">{{ _("Reset Password") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://people.ipfire.org/password-reset/{{ account.uid }}/{{ reset_code }}">
+                                                       {{ _("Reset Password") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
+               </td>
+       </tr>
 {% end block %}
index 1c9fa0fa1f8db504718528e1ffd096cd45d4194f..4505f4f225791e91fa3dabd613661e76e529c096 100644 (file)
@@ -1,43 +1,37 @@
 {% extends "../../messages/base.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Hello once again, %s,") % account.first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hello once again, %s,") % account.first_name }}</h1>
 
-    <p>
-        {{ _("we hope you are enjoying using IPFire.") }}
-    </p>
+                       <p>
+                               {{ _("we hope you are enjoying using IPFire.") }}
+                       </p>
 
-    <p>
-        {{ _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
-        {{ _("People like me often post on here, providing help and support.") }}
-    </p>
+                       <p>
+                               {{ _("Did you know that you can get help from our community at https://community.ipfire.org?") }}
+                               {{ _("People like me often post on here, providing help and support.") }}
+                       </p>
 
-    <p>
-        {{ _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
-    </p>
+                       <p>
+                               {{ _("But we also rely on you donations. Please consider helping us by setting up a small monthly donation:") }}
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate?frequency=monthly" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://www.ipfire.org/donate?frequency=monthly">
+                                                       {{ _("Donate Now") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
 
-    <p>
-        {{ _("Thank you, we really appreciate your support,") }}
-        <br>{{ _("-Arne")}}
-    </p>
+                       <p>
+                               {{ _("Thank you, we really appreciate your support,") }}
+                               <br>{{ _("-Arne")}}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index 3ab3275a3c44508ea6193ab983aacfe16f2e9045..4b17f9c24b16cb0a7dcd39c0c0ea59e4198b0ea1 100644 (file)
@@ -1,47 +1,41 @@
 {% extends "../../messages/base.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Hello %s!") % account.first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+               <h1>{{ _("Hello %s!") % account.first_name }}</h1>
 
-    <p>
-        {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
-    </p>
+                       <p>
+                               {{ _("I would like to introduce myself: I'm Michael and I am one of the people behind the project. We are a team of passionate people who try to make the Internet a better place. On behalf of everyone, I would like to say: Welcome to the IPFire Project!") }}
+                       </p>
 
-    <p>
-        {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
-        {{ _("To do this, please log on to your profile and click the edit button.") }}
-    </p>
+                       <p>
+                               {{ _("We want you to feel a part of our team. Can I ask you to set up your profile? We would love to know a little bit more about you.") }}
+                               {{ _("To do this, please log on to your profile and click the edit button.") }}
+                       </p>
 
-    <p>
-        {{ _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
-    </p>
+                       <p>
+                               {{ _("I would also like to invite you to join our community at https://community.ipfire.org, if you haven't already done so.") }}
+                       </p>
 
-    <p>
-        {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
-    </p>
+                       <p>
+                               {{ _("Finally, this organisation relies on the generous donations of people like you. If you can, please consider supporting this project and the team behind it, so that we can continue our long-term vision, fund developers and promote our project.") }}
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://www.ipfire.org/donate">
+                                                       {{ _("Donate Now") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
 
-    <p>
-        {{ _("Thank you,") }}
-        <br>{{ _("-Michael") }}
-    </p>
+                       <p>
+                               {{ _("Thank you,") }}
+                               <br>{{ _("-Michael") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index 6c983dc7b81193853063553e5917000743cfa177..bd3fc29945dcb7e7a840dbe6233f1dfa3d4d25b2 100644 (file)
@@ -1,33 +1,36 @@
 {% extends "../../messages/base.html" %}
 
+{% block hero %}
+       <tr class="hero">
+               <td>
+                       <img class="g-img" src="{{ embed_image("img/auth/register@600.jpg") }}" alt="IPFire">
+               </td>
+       </tr>
+{% end block %}
+
 {% block content %}
-    <p>
-        <strong>{{ _("Hello %s!") % first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hello %s!") % first_name }}</h1>
 
-    <p>
-        {{ _("Thank you for registering a new account with us.") }}
-    </p>
+                       <p>
+                               {{ _("Thank you for registering a new account with us.") }}
+                       </p>
 
-    <p>
-        {{ _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }} {{ _("There are many things you can do with your account.") }}
-    </p>
+                       <p>
+                               {{ _("This account will allow you to take part in our project. Either by joining conversations, writing documentation, or becoming a developer.") }}
+                               {{ _("There are many things you can do with your account.") }}
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/activate/{{ uid }}/{{ activation_code }}" target="_blank">{{ _("Activate Account") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://www.ipfire.org/activate/{{ uid }}/{{ activation_code }}">
+                                                       {{ _("Activate Account") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
+               </td>
+       </tr>
 {% end block %}
index d1471f576513ba97230d3003e2a8a0e8ee2ab1ef..d8d223cc7c54d7cfc4a73873e46a99b16cb11d7e 100644 (file)
@@ -7,10 +7,10 @@
                <div class="container">
                        <div class="columns is-centered">
                                <div class="column is-one-third">
-                                       <h1 class="title is-1">
+                                       <h1 class="title">
                                                IPFire<span class="has-text-primary">_</span>
                                        </h1>
-                                       <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+                                       <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
 
                                        <div class="block">
                                                <form action="" method="POST">
index 5075c8ee90d79b7d809b8def9381bab184b8de52..f11a5342caa2d6259207cf3b4f274fb19154861d 100644 (file)
@@ -7,10 +7,10 @@
                <div class="container">
                        <div class="columns is-centered">
                                <div class="column is-one-third">
-                                       <h1 class="title is-1">
+                                       <h1 class="title">
                                                IPFire<span class="has-text-primary">_</span>
                                        </h1>
-                                       <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+                                       <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
 
                                        <p class="is-size-5">{{ _("You will shortly receive an email with instructions on how to reset your password.") }}</p>
                                </div>
index a7d45b6746bfd60815b321849ef7981a884bd5ed..f7cfc2f508243415daf49c759721c9b7da2a1d91 100644 (file)
@@ -7,10 +7,10 @@
                <div class="container">
                        <div class="columns is-centered">
                                <div class="column is-one-third">
-                                       <h1 class="title is-1">
+                                       <h1 class="title">
                                                IPFire<span class="has-text-primary">_</span>
                                        </h1>
-                                       <h4 class="subtitle is-4">{{ _("Reset Your Password") }}</h4>
+                                       <h4 class="subtitle">{{ _("Reset Your Password") }}</h4>
 
                                        <div class="block">
                                                <form action="" method="POST">
index 7c2e47c1b84c7a351272a903fa69648d1f108245..c3726bcd68c33b9677ed82ea3d1fc10851eaaeaf 100644 (file)
@@ -7,10 +7,10 @@
        <div class="container">
                <div class="columns is-centered">
                        <div class="column is-one-third">
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        IPFire<span class="has-text-primary">_</span>
                                </h1>
-                               <h4 class="subtitle is-4">
+                               <h4 class="subtitle">
                                        {{ _("Your account has been created.") }}
                                        {{ _("Please check your email for next steps.") }}
                                </h4>
index 30aeb5bc0df0b5da2cfb028ddb09a516bd7d5603..9cf067ead8b5e6cb7f2b7e2b9ae9b592b3b87f05 100644 (file)
@@ -7,10 +7,10 @@
                <div class="container">
                        <div class="columns is-centered">
                                <div class="column is-one-third">
-                                       <h1 class="title is-1">
+                                       <h1 class="title">
                                                IPFire<span class="has-text-primary">_</span>
                                        </h1>
-                                       <h4 class="subtitle is-4">{{ _("Register A New Account") }}</h4>
+                                       <h4 class="subtitle">{{ _("Register A New Account") }}</h4>
 
                                        <div class="block">
                                                <p class="is-size-5">
index 1e3d00277636c844004a160b6fa0e3f49745c9b0..d5446cefa856b310e8d1a5273e229c2873aaca28 100644 (file)
                                        <a class="navbar-item is-size-4" href="/">
                                                <strong>
                                                        {% if request.path.startswith("/projects/location") %}
-                                                               IPFire<span class="has-text-primary">_</span>Location
+                                                               {% module IPFireLogo("Location") %}
                                                        {% elif hostname.startswith("fireinfo.") %}
-                                                               IPFire<span class="has-text-primary">_</span>Fireinfo
+                                                               {% module IPFireLogo("Fireinfo") %}
                                                        {% elif hostname.startswith("nopaste.") %}
-                                                               IPFire<span class="has-text-primary">_</span>Nopaste
+                                                               {% module IPFireLogo("nopaste") %}
                                                        {% else %}
-                                                               IPFire<span class="has-text-primary">_</span>
+                                                               {% module IPFireLogo() %}
                                                        {% end %}
                                                </strong>
                                        </a>
@@ -79,7 +79,7 @@
                                                                                                <div class="control has-icons-left">
                                                                                                        <input class="input" type="text"
                                                                                                                name="q" {% if "q" in locals() and q %}value="{{ q }}"{% end %}
-                                                                                                               placeholder="{{ _("Search Docs...") }}">
+                                                                                                               placeholder="{{ _("Search Documentation...") }}">
                                                                                                        <span class="icon is-small is-left">
                                                                                                                <i class="fas fa-search"></i>
                                                                                                        </span>
 
                                                                <div class="navbar-item">
                                                                        <a class="button is-primary has-text-weight-bold is-uppercase"
-                                                                                       href="https://www.ipfire.org/donate">
+                                                                                       href="/donate">
                                                                                {{ _("Donate") }}
                                                                        </a>
                                                                </div>
+
+                                                               {% if current_user %}
+                                                                       <div class="navbar-item has-dropdown is-hoverable">
+                                                                               <a class="navbar-link is-arrowless" href="/users/{{ current_user.uid }}">
+                                                                                       <figure class="image">
+                                                                                               <img class="is-rounded" style="width: auto" src="{{ current_user.avatar_url(128) }}">
+                                                                                       </figure>
+                                                                               </a>
+
+                                                                               <div class="navbar-dropdown">
+                                                                                       <a class="navbar-item" href="/users/{{ current_user.uid }}/passwd">
+                                                                                               {{ _("Change Password") }}
+                                                                                       </a>
+
+                                                                                       <hr class="navbar-divider">
+
+                                                                                       <a class="navbar-item" href="/logout">
+                                                                                               {{ _("Logout")}}
+                                                                                       </a>
+                                                                               </div>
+                                                                       </div>
+                                                               {% end %}
                                                        </div>
-                                               {% elif hostname == "fireinfo.ipfire.org" %}
+                                               {% elif hostname.startswith("fireinfo.") %}
                                                        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
                                                                        aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
                                                                <span class="fas fa-bars"></span>
                                                                        </li>
                                                                </ul>
                                                        </div>
-                                               {% elif hostname == "location.ipfire.org" %}
-                                                       <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
-                                                                       aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
-                                                               <span class="fas fa-bars"></span>
-                                                       </button>
-
-                                                       <div class="collapse navbar-collapse" id="navbar">
-                                                               <ul class="navbar-nav ml-auto">
-                                                                       <li class="nav-item">
-                                                                               <a class="nav-link {% if request.path == "/how-to-use" %}is-active{% end %}" href="/how-to-use">
-                                                                                       {{ _("How To Use") }}
-                                                                               </a>
-                                                                       </li>
-
-                                                                       <li class="nav-item">
-                                                                               <a class="nav-link {% if request.path == "/download" %}is-active{% end %}" href="/download">
-                                                                                       {{ _("Download") }}
-                                                                               </a>
-                                                                       </li>
-                                                               </ul>
-
-                                                               <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
-                                                                       {{ _("Donate") }}
-                                                               </a>
-                                                       </div>
-                                               {% elif hostname == "nopaste.ipfire.org" %}
+                                               {% elif hostname.startswith("nopaste.") %}
                                                        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
                                                                        aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
                                                                <span class="fas fa-bars"></span>
                {% block footer %}
                        <footer class="footer is-flex-shrink-0">
                                <div class="container">
-                                       <div class="columns">
-                                               <div class="column is-two-fifths">
-                                                       {# Show some profile information for users who are logged in #}
-                                                       {% if current_user %}
-                                                               <h4 class="title is-4 mb-0">{{ _("Hello, %s!") % current_user }}</h4>
-
-                                                               <div class="level is-mobile">
-                                                                       <div class="level-left">
-                                                                               <a class="level-item" href="/users/{{ current_user.uid }}">
-                                                                                       {{ _("My Profile") }}
-                                                                               </a>
+                                       {# Encourage people to join #}
+                                       {% if not current_user %}
+                                               <div class="columns">
+                                                       <div class="column is-one-fifth">
+                                                               <a class="button is-primary is-medium is-fullwidth" href="/register">
+                                                                       {{ _("Join Now") }}
+                                                               </a>
+                                                       </div>
 
-                                                                               <a class="level-item" href="/logout">
-                                                                                       {{ _("Logout") }}
-                                                                               </a>
-                                                                       </div>
+                                                       <div class="column is-one-quarter">
+                                                               <div class="block">
+                                                                       <p class="title is-5">
+                                                                               {{ _("Join our community and sign up to our newsletter") }}
+                                                                       </p>
                                                                </div>
-
-                                                       {# Otherwise encourage people to join #}
-                                                       {% else %}
-                                                               <p class="is-size-4">
-                                                                       <span class="has-text-weight-bold">IPFire</span><span class="has-text-primary has-text-weight-bold">_</span>People
-                                                               </p>
-
-                                                               <p>
-                                                                       Join the community and sign up for our newsletter
-                                                               </p>
-
-                                                               <a class="button is-primary is-outlined is-medium
-                                                                               has-text-black has-text-weight-bold" href="/register">
-                                                                       JOIN NOW
-                                                               </a>
-                                                               <a class="button is-primary is-outlined  is-medium
-                                                                               has-text-black has-text-weight-bold" href="/login">
-                                                                       LOG IN
-                                                               </a>
-                                                       {% end %}
+                                                       </div>
                                                </div>
 
-                                               <div class="column">
-                                                       <ul>
-                                                               <li>
-                                                                       <a href="/about">
-                                                                               {{ _("About") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="/docs">
-                                                                               {{ _("Documentation") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="/help">
-                                                                               {{ _("Help") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="/docs/devel">
-                                                                               {{ _("Development") }}
-                                                                       </a>
-                                                               </li>
-                                                       </ul>
+                                               <div class="columns">
+                                                       <div class="column is-one-fifth">
+                                                               <a class="button is-primary is-medium is-outlined is-fullwidth" href="/login">
+                                                                       {{ _("Log In") }}
+                                                               </a>
+                                                       </div>
                                                </div>
+                                       {% end %}
 
-                                               <div class="column">
-                                                       <ul>
-                                                               <li>
-                                                                       <a href="/download">
-                                                                               {{ _("Download") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="https://community.ipfire.org/">
-                                                                               {{ _("Community") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="/sitemap">
-                                                                               {{ _("Sitemap") }}
-                                                                       </a>
-                                                               </li>
-                                                               <li>
-                                                                       <a href="/legal">
-                                                                               {{ _("Legal") }}
-                                                                       </a>
-                                                               </li>
-                                                       </ul>
-                                               </div>
+                                               <div class="level">
+                                                       <div class="level-left">
+                                                               <div class="level-item">
+                                                                       {{ year }} &copy; IPFire.org
+                                                               </div>
 
-                                               {% if current_user and current_user.is_staff() %}
-                                                       <div class="column">
-                                                               <ul>
-                                                                       <li>
-                                                                               <a href="/voip">
-                                                                                       {{ _("VoIP") }}
-                                                                               </a>
-                                                                       </li>
-                                                               </ul>
-                                                       </div>
-                                               {% end %}
+                                                               <div class="level-item">
+                                                                       <a href="/legal">{{ _("Legal") }}</a>
+                                                               </div>
 
-                                               <div class="column is-one-fifth">
-                                                       <div class="block">
-                                                               <a class="button is-primary is-fullwidth is-medium has-text-weight-bold is-uppercase"
-                                                                               href="https://www.ipfire.org/donate">
-                                                                       {{ _("Donate") }}
-                                                               </a>
+                                                               <div class="level-item">
+                                                                       <a href="/sitemap">{{ _("Sitemap") }}</a>
+                                                               </div>
                                                        </div>
 
-                                                       <div class="block">
-                                                               <div class="level is-mobile">
-                                                                       <div class="level-item">
-                                                                               <a href="https://social.ipfire.org/@news" title="{{ _("Mastodon") }}">
-                                                                                       <i class="fa-brands fa-mastodon"></i>
-                                                                               </a>
-                                                                       </div>
-                                                                       <div class="level-item">
-                                                                               <a href="https://twitter.com/ipfire" title="{{ _("Twitter") }}">
-                                                                                       <i class="fa-brands fa-twitter"></i>
-                                                                               </a>
-                                                                       </div>
-                                                                       <div class="level-item">
-                                                                               <a href="https://linkedin.com/company/ipfire" title="{{ _("LinkedIn") }}">
-                                                                                       <i class="fa-brands fa-linkedin-in"></i>
-                                                                               </a>
-                                                                       </div>
+                                                       <div class="level-right">
+                                                               <div class="level-item">
+                                                                       <a href="https://social.ipfire.org/@news" title="{{ _("Mastodon") }}">
+                                                                               <i class="fa-brands fa-mastodon px-2"></i>
+                                                                       </a>
+                                                               </div>
+                                                               <div class="level-item">
+                                                                       <a href="https://twitter.com/ipfire" title="{{ _("Twitter") }}">
+                                                                               <i class="fa-brands fa-twitter px-2"></i>
+                                                                       </a>
+                                                               </div>
+                                                               <div class="level-item">
+                                                                       <a href="https://linkedin.com/company/ipfire" title="{{ _("LinkedIn") }}">
+                                                                               <i class="fa-brands fa-linkedin-in px-2"></i>
+                                                                       </a>
                                                                </div>
                                                        </div>
                                                </div>
-                                       </div>
                                </div>
                        </footer>
                {% end block %}
index 0513ea639c52d2205fd3183a1c958d700400b688..6f0a055fd68a43c8037271c47221a302d6fb111f 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">{{ _("Home") }}</a>
@@ -20,7 +20,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("My Drafts") }}</h1>
+                               <h1 class="title">{{ _("My Drafts") }}</h1>
                        </div>
                </div>
        </section>
index 57fdbf62f95c10c2d5ce52cf4439fe97d0b14d82..678f9e840cfeedf5c1e666673c8ff2f765111e9f 100644 (file)
@@ -13,7 +13,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">{{ _("Home") }}</a>
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("IPFire Blog") }}</h1>
+                               <h1 class="title">{{ _("IPFire Blog") }}</h1>
 
                                {% if q %}
-                                       <h6 class="subtitle is-5">
+                                       <h6 class="subtitle">
                                                {{ _("Search Results for '%s'") % q }}
                                        </h6>
                                {% end %}
                                                        </div>
                                                </div>
                                        </div>
-
-                                       {% module BlogHistoryNavigation() %}
                                </div>
                        </div>
+
+                       {# Show links to older years... #}
+                       {% module BlogHistoryNavigation() %}
                </div>
        </section>
 {% end block %}
index 5e039be10b36a61664a48ee135e0ff517358bd20..62971aa09c5d9d81422ad8d73475de7a0256b11e 100644 (file)
@@ -1,33 +1,30 @@
 {% extends "../../messages/base-promo.html" %}
 
-{% block content %}
-    <p>
-        {{ _("there is a new post from %s on the IPFire Blog:") % post.author }}
-    </p>
+{% block title %}{{ post.title }}{% end block %}
+
+{# Preview text shown in the inbox preview... #}
+{% block preview %}{{ post.excerpt }}{% end block %}
 
-    <p>
-        <strong>{{ post.title }}</strong>
-    </p>
+{% block content %}
+       <tr class="section">
+               <td>
+                       <h1>
+                               {{ post.title }}
+                       </h1>
 
-    {% if post.excerpt %}
-        <blockquote>{{ post.excerpt }}</blockquote>
-    {% end %}
+                       <p>
+                               {{ post.excerpt }}
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/blog/{{ post.slug }}" target="_blank">{{ _("Click Here To Read More") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                                <a class="primary" href="https://www.ipfire.org/blog/{{ post.slug }}">
+                                                       {{ _("Read The Full Post On Our Blog") }}
+                                                </a>
+                                       </td>
+                               </tr>
+                       </table>
+               </td>
+       </tr>
 {% end block %}
index 7ea0b643c0f1286f3acd7d3dcbe4ef6cfb1e611b..0d82502c0095b5e8251b674866dc54860e4a28f8 100644 (file)
@@ -1,9 +1,13 @@
-<h5 class="title is-5">{{ _("Years") }}</h6>
+<div class="level">
+       <div class="level-left">
+               <div class="level-item">
+                       {{ _("Archive") }}
+               </div>
 
-<ul>
-       {% for year in years %}
-               <li>
-                       <a class="nav-link" href="/blog/years/{{ year }}">{{ year }}</a>
-               </li>
-       {% end %}
-</ul>
+               {% for year in years %}
+                       <div class="level-item">
+                               <a href="/blog/years/{{ year }}">{{ year }}</a>
+                       </div>
+               {% end %}
+       </div>
+</div>
index 33ba7bf8649ae0418386ea2d9356137c70a9c888..201592063d082707b2f3e1128db616a9bfd72850 100644 (file)
@@ -6,7 +6,7 @@
                        </a>
                </h5>
 
-               <h6 class="subtitle is-6">
+               <h6 class="subtitle">
                        {% if post.published_at %}
                                {{ locale.format_date(post.published_at, shorter=True, relative=relative) }}
                        {% elif post.created_at %}
index b5fcc8d37ffdf9cbe163a1a2733c19244b46104a..f057fe5e0854847c27a787611c2b6f6a18abcd82 100644 (file)
@@ -40,7 +40,7 @@
        <section class="hero {% if "lightningwirelabs.com" in post.tags %}is-lwl{% elif post.is_published() %}is-primary{% else %}is-light{% end %}">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">{{ _("Home") }}</a>
@@ -54,9 +54,9 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ post.title }}</h1>
+                               <h1 class="title">{{ post.title }}</h1>
 
-                               <h6 class="subtitle is-6">
+                               <h6 class="subtitle">
                                        {{ _("by") }}
 
                                        {% if isinstance(post.author, accounts.Account) %}
                </div>
        </section>
 
+       {# Encourage people to sign up & subscribe... #}
+       {% if not current_user or not current_user.consents_to_promotional_emails %}
+               <section class="has-background-light">
+                       <div class="container">
+                               <p class="has-text-centered px-2 py-1">
+                                       {{ _("Do you like what you are reading?") }}
+                                       {{ _("Subscribe to our newsletter and don't miss out on the latest...") }}
+
+                                       &nbsp;
+
+                                       {% if not current_user %}
+                                               <a class="has-text-weight-bold" href="/register">
+                                                       {{ _("Join Now") }}
+                                               </a>
+                                       {% else %}
+                                               <a class="has-text-weight-bold" href="/subscribe">
+                                                       {{ _("Subscribe Now") }}
+                                               </a>
+                                       {% end %}
+                               </p>
+                       </div>
+               </section>
+       {% end %}
+
        <section class="section">
                <div class="container">
                        <div class="columns is-justify-content-space-between">
index ad6eb6d6b4459440aa907c0f6f208bcb8ae2b0cc..b953efec6dae6c64965840c2b52eac895f759ae6 100644 (file)
@@ -12,7 +12,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">{{ _("Home") }}</a>
@@ -26,7 +26,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Write a New Post") }}</h1>
+                               <h1 class="title">{{ _("Write a New Post") }}</h1>
                        </div>
                </div>
        </section>
index 31a01de3f8431df213f845270e9b1b3e624b5cef..b9339153852db9af43ef2e545e2bdd22347ab2d0 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero has-background-primary-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -19,7 +19,7 @@
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("Posts in %s") % year }}
                                </h1>
                        </div>
                                <div class="column is-8">
                                        {% module BlogList(posts) %}
                                </div>
-                               <div class="column">
-                                       {% module BlogHistoryNavigation() %}
-                               </div>
                        </div>
+
+                       {% module BlogHistoryNavigation() %}
                </div>
        </section>
 {% end block %}
index e6d416bc7eab376dc0508f6a04ceb9718f366907..766f79a1f428816875f0dae3008239c4a4d51654 100644 (file)
@@ -1,7 +1,7 @@
 <section class="hero is-light">
        <div class="hero-body">
                <div class="container">
-                       <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                       <nav class="breadcrumb" aria-label="breadcrumbs">
                                <ul>
                                        <li>
                                                <a href="/">
@@ -11,7 +11,7 @@
 
                                        <li>
                                                <a href="/docs">
-                                                       {{ _("Docs") }}
+                                                       {{ _("Documentation") }}
                                                </a>
                                        </li>
 
@@ -37,7 +37,7 @@
                                </ul>
                        </nav>
 
-                       <h1 class="title is-1">{{ page_title or _("IPFire Documentation") }}</h1>
+                       <h1 class="title">{{ page_title or _("IPFire Documentation") }}</h1>
                </div>
        </div>
 </section>
index 6808bca22bfa83c2936caebeda803d074ae2a897..27a179fa7d62636c954434c79c951c4b59cbc71c 100644 (file)
@@ -5,7 +5,7 @@
 {% block content %}
        <div class="container">
                <section class="section">
-                       <h1 class="title is-1">{{ _("Recent Changes") }}</h1>
+                       <h1 class="title">{{ _("Recent Changes") }}</h1>
 
 
                        {% module DocsList(recent_changes, show_changes=True) %}
index d880735cea07f120e266807db6b0528a53568092..2ba57f74ab56d1b4e252d2b5ba3926e2ae06bc68 100644 (file)
@@ -6,21 +6,21 @@
        <section class="hero is-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">{{ _("Home") }}</a>
                                                </li>
                                                <li class="is-active">
-                                                       <a href="#" aria-current="page">{{ _("Docs") }}</a>
+                                                       <a href="#" aria-current="page">{{ _("Documentation") }}</a>
                                                </li>
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("IPFire Docs") }}</h1>
+                               <h1 class="title">{{ _("IPFire Documentation") }}</h1>
 
                                {% if q %}
-                                       <h6 class="subtitle is-5s">
+                                       <h6 class="subtitle">
                                                {{ _("Search Results for '%s'") % q }}
                                        </h6>
                                {% end %}
index 2297d77ca62f51e02581d31f9052473f878c865d..b67b5f42b967f8322f53dc176b1b42f8be84fee2 100644 (file)
@@ -8,7 +8,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -18,7 +18,7 @@
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        Donate
                                </h1>
                                <p class="subtitle">{{ _("Please support our project with your donation today") }}</p>
                                                                <div class="control">
                                                                        <input type="text" class="input" name="first_name"
                                                                                placeholder="{{ _("First Name" )}}" required
-                                                                               {% if first_name %}value="{{ first_name }}"{% end %}>
+                                                                               {% if current_user %}value="{{ current_user.first_name }}"{% end %}>
                                                                </div>
                                                        </div>
 
                                                                <div class="control">
                                                                        <input type="text" class="input" name="last_name"
                                                                                placeholder="{{ _("Last Name" )}}" required
-                                                                               {% if last_name %}value="{{ last_name }}"{% end %}>
+                                                                               {% if current_user %}value="{{ current_user.last_name }}"{% end %}>
                                                                </div>
                                                        </div>
                                                </div>
                                                <div class="block">
                                                        <div class="control">
                                                                <input type="email" class="input" name="email"
-                                                                       placeholder="{{ _("Email Address") }}" required>
+                                                                       placeholder="{{ _("Email Address") }}" required
+                                                                       {% if current_user %}value="{{ current_user.email }}"{% end %}>
                                                        </div>
                                                </div>
 
+                                               {% set lines = current_user.street.splitlines() if current_user else [] %}
+
                                                <div class="block">
                                                        <div class="control">
                                                                <input type="text" class="input" name="street1"
-                                                                       placeholder="{{ _("Address Line 1") }}" required>
+                                                                       placeholder="{{ _("Address Line 1") }}" required
+                                                                       {% if lines %}value="{{ lines[0] }}"{% end %}>
                                                        </div>
                                                </div>
 
                                                <div class="block">
                                                        <div class="control">
                                                                <input type="text" class="input" name="street2"
-                                                                       placeholder="{{ _("Address Line 2") }}">
+                                                                       placeholder="{{ _("Address Line 2") }}"
+                                                                       {% if lines and len(lines) > 1 %}value="{{ lines[1] }}"{% end %}>
                                                        </div>
                                                </div>
 
                                                        <div class="column">
                                                                <div class="control">
                                                                        <input type="text" class="input" name="city"
-                                                                               placeholder="{{ _("City") }}" required>
+                                                                               placeholder="{{ _("City") }}" required
+                                                                               {% if current_user %}value="{{ current_user.city }}"{% end %}>
                                                                </div>
                                                        </div>
                                                        <div class="column">
                                                                <div class="control">
                                                                        <input type="text" class="input" name="post_code"
-                                                                               placeholder="{{ _("Post Code") }}" required>
+                                                                               placeholder="{{ _("Post Code") }}" required
+                                                                               {% if current_user %}value="{{ current_user.postal_code }}"{% end %}>
                                                                </div>
                                                        </div>
                                                </div>
index 01a59af0da8f6438db4a530b80cb971fff9e1ece..b0657ebe542c02b7aa8d483dae99c134dee029b5 100644 (file)
@@ -1,62 +1,43 @@
 {% extends "../../messages/base-promo.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Dear %s,") % account.first_name }}</strong>
-    </p>
-
-    <p>
-        As we approach the end of the year, now is a good time to look back and reflect on
-        the progress of our project to celebrate our achievements and decide where we need
-        to redouble our efforts in the coming months.
-    </p>
-
-    <p>
-        We’ve launched nine packed Core Updates to a userbase of millions of people in
-        almost 200 countries.
-        We’ve developed and improved on our firewall engine adding support for blocking
-        any connections from hostile networks, added Two-Factor Authentication for OpenVPN,
-        and massively improved our Intrusion Prevention System and Quality of Service.
-    </p>
-
-    <p>
-        As we move into the New Year, in addition to our standard Core Updates,
-        we’re looking to bring our software to even more users
-        and we strive to continue being more agile than the big boys in the industry.
-    </p>
-
-       <p>
-        2022 has been a challenging year and with an Open Source project,
-        it is increasingly difficult to meet the cost of running it.
-        Can you help us meet these costs?
-    </p>
-
-    <p>
-               If you’re able to help, please click the link below -
-               your donation ensures the longevity of our project, and gives us the ability
-               to keep our product free and open to all!
-    </p>
-
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
-
-    <p>
-        {{ _("All the best,") }}
-        <br>{{ _("-Your IPFire Team") }}
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hello %s,") % account.first_name }}</h1
+
+                       <p>
+                               Just like the last couple of years, 2023 has not been without it’s challenges. Here
+                               at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+                               from our friends and colleagues across the globe and the progress we've made on
+                               our project.
+                       </p>
+
+                       <p>
+                               We want to spread positivity and joy so why not get in touch on
+                               Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+                               LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+                               or why you like working with IPFire?
+                               We’ll be sharing and retweeting some of our favourites in the weeks to come.
+                       </p>
+
+                       <p>
+                               If you really liked working with IPFire this year, why not consider donating to our project?
+                       </p>
+
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                               <a class="primary" href="https://www.ipfire.org/donate">
+                                                       {{ _("Donate") }}
+                                               </a>
+                                       </td>
+                               </tr>
+                       </table>
+
+                       <p>
+                               {{ _("All the best,") }}
+                               <br>{{ _("-Your IPFire Team") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index 83a713774d488d2eba6e57ea2070e11d1c4819f4..63e93677be8a9207b60d1d82565ee1b472b282d4 100644 (file)
@@ -1,32 +1,23 @@
 From: IPFire Project <no-reply@ipfire.org>
 To: {{ account.email_to }}
-Subject: {{ _("Another year over. Let's celebrate 2022!") }}
+Subject: {{ _("IPFire Highlights of 2023") }}
 Precedence: bulk
 X-Auto-Response-Suppress: OOF
 
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hello %s,") % account.first_name }}
 
-As we approach the end of the year, now is a good time to look back and reflect on
-the progress of our project to celebrate our achievements and decide where we need
-to redouble our efforts in the coming months.
+Just like the last couple of years, 2023 has not been without it’s challenges. Here
+at IPFire, we’re looking back at the year and highlighting the positivity we’ve had
+from our friends and colleagues across the globe and the progress we've made on
+our project.
 
-We’ve launched nine packed Core Updates to a userbase of millions of people in
-almost 200 countries.
-We’ve developed and improved on our firewall engine adding support for blocking
-any connections from hostile networks, added Two-Factor Authentication for OpenVPN,
-and massively improved our Intrusion Prevention System and Quality of Service.
+We want to spread positivity and joy so why not get in touch on
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) or
+LinkedIn (https://www.linkedin.com/company/ipfire) and share your highlights of the year
+or why you like working with IPFire?
+We’ll be sharing and retweeting some of our favourites in the weeks to come.
 
-As we move into the New Year, in addition to our standard Core Updates,
-we’re looking to bring our software to even more users
-and we strive to continue being more agile than the big boys in the industry.
-
-2022 has been a challenging year and with an Open Source project,
-it is increasingly difficult to meet the cost of running it.
-Can you help us meet these costs?
-
-If you’re able to help, please click the link below -
-your donation ensures the longevity of our project, and gives us the ability
-to keep our product free and open to all!
+If you really liked working with IPFire this year, why not consider donating to our project?
 
  https://www.ipfire.org/donate
 
index d82787d5a5a7a3984d9d738e024cbdc0388a197a..51fef986c8d508970642e54d9dc7bd9f0a272119 100644 (file)
@@ -1,49 +1,48 @@
 {% extends "../../messages/base-promo.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Dear %s,") % account.first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hi %s,") % account.first_name }}</h1
 
-       <p>
-               When we launched our little project all those years ago,
-               never did we think we would reach so far around the world.
-               We sometimes wonder if Father Christmas is using us at the North Pole
-               just like his penguin friends at the South Pole!
-       </p>
+                       <p>
+                               For so many people around the world, the end of the calendar year is party season and
+                               at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+                               We’re celebrating what’s to come next year, of course!
+                       </p>
 
-       <p>
-               Our Christmas list this year isn’t very long -
-               we’re asking for donations to the project to ensure that our IPFire users,
-               just like you, can continue making the most of our free-to-use project.
-       </p>
+                       <p>
+                               Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+                               the following:
+                       </p>
 
-       <p>
-               We know some people can’t donate and that’s cool but if you can,
-               even just the cost of a beer, it would help us to secure the project
-               for the year to come.
-       </p>
+                       <ul>
+                               <li>Relaunch of the IPFire website, helping us promote our mission and improve our outreach.</li>
+                               <li>Improve usability by making documentation and user guide more accessible.</li>
+                               <li>Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+                                       suggestions for improvements to our code.</li>
+                       </ul>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <p>
+                               Of course, we’d not be able to do any of this without feedback from our huge user base
+                               and our fantastic development team. If you’d like to help our development team towards
+                               their goals for next year, you can donate here:
+                       </p>
 
-    <p>
-        {{ _("Thank you for supporting the project,") }}
-        <br>{{ _("-Your IPFire Team") }}
-    </p>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                               <a class="primary" href="https://www.ipfire.org/donate">
+                                                       {{ _("Donate") }}
+                                               </a>
+                                       </td>
+                               </tr>
+                       </table>
+
+                       <p>
+                               {{ _("Party, party, party!") }}
+                               <br>{{ _("-Your IPFire Team") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index df1731711e549ab33e691d5d0e1bb5b6848a9a92..c1d708ad6bea9aae73bc43588f8da9261d33846f 100644 (file)
@@ -1,27 +1,30 @@
 From: IPFire Project <no-reply@ipfire.org>
 To: {{ account.email_to }}
-Subject: {{ _("Did you know IPFire is being used on all seven continents?") }}
+Subject: {{ _("December is Party Season!") }}
 Precedence: bulk
 X-Auto-Response-Suppress: OOF
 
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
 
-When we launched our little project all those years ago,
-never did we think we would reach so far around the world.
-We sometimes wonder if Father Christmas is using us at the North Pole
-just like his penguin friends at the South Pole!
+For so many people around the world, the end of the calendar year is party season and
+at IPFire we’re celebrating as much as anyone else. Why are we celebrating?
+We’re celebrating what’s to come next year, of course!
 
-Our Christmas list this year isn’t very long -
-we’re asking for donations to the project to ensure that our IPFire users,
-just like you, can continue making the most of our free-to-use project.
+Thanks to some super-hardwork by our team in 2023, over the next 12 months, you’ll see
+the following:
 
-We know some people can’t donate and that’s cool but if you can,
-even just the cost of a beer, it would help us to secure the project
-for the year to come.
+  * Relaunch of the IPFire website, helping us promote our mission and improve our outreach.
+  * Improve usability by making documentation and user guide more accessible.
+  * Helping you, the users, to contribute your guidance to the IPFire Wiki and make
+    suggestions for improvements to our code.
+
+Of course, we’d not be able to do any of this without feedback from our huge user base
+and our fantastic development team. If you’d like to help our development team towards
+their goals for next year, you can donate here:
 
   https://www.ipfire.org/donate
 
-{{ _("Thank you for supporting the project,") }}
+Party, party, party!
 -{{ _("Your IPFire Team") }}
 
 --
index 853fb05a71e3ef99eda42fa74956e7957592ba91..235f50f8c5b34567fc7616ff3e1e332ed38f1cd2 100644 (file)
@@ -1,53 +1,48 @@
 {% extends "../../messages/base-promo.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Dear %s,") % account.first_name }}</strong>
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Dear %s,") % account.first_name }}</h1>
 
-    <p>
-       With the holiday season upon us, we hope you’re enjoying the festive treats
-       and magical feeling that comes with this time of year.
-    </p>
+                       <p>
+                               The year has drawn to an end and we just wanted to take a moment to say thank you.
+                               Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+                               taken the time to be part of our ever-growing community.
+                       </p>
 
-       <p>
-       While you’re enjoying the festivities, it might be time to spare a thought for
-       your IPFire appliance - whether in a cupboard or under a desk or sitting cool
-       as a cucumber in your comms room, she’s been working away all year to look after you!
-    </p>
+                       <p>
+                               We’re celebrating our best year yet including the recent success of our developer summit
+                               where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+                               in 2023 though! We had may contributors who helped us to release 10 updates to
+                               millions of users across every continent.
+                               Yes, even our penguin friends in the antarctic are using IPFire!
+                       </p>
 
-       <p>
-               We would love if you could help us with a donation towards the IPFire project
-               to keep our infrastructure running so your hardworking appliance can continue
-               to keep you safe!
-    </p>
+                       <p>
+                               We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+                               the love and you can spare a little money, help us start 2024 with a bang!
+                       </p>
 
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate?frequency=monthly&amp;amount=10" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                               <a class="primary" href="https://www.ipfire.org/donate">
+                                                       {{ _("Donate") }}
+                                               </a>
+                                       </td>
+                               </tr>
+                       </table>
 
-       <p>
-               Always remember to show appreciation to your IPFire appliance.
-               Remember to keep her updated with the latest firmware and please don’t share your
-               festive treats with her - most firewalls don’t enjoy pastry crumbs!
-       </p>
+                       <p>
+                               Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+                               for the festive season.
+                       </p>
 
-    <p>
-        {{ _("All the best,") }}
-        <br>{{ _("-Your IPFire Team") }}
-    </p>
+                       <p>
+                               {{ _("Thank you,") }}
+                               <br>{{ _("-Your IPFire Team") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index dcc0638cf851f1510b165c48bc8cc66d0a88af74..f2c154b1ed2b203725dee09da8881635bf690f38 100644 (file)
@@ -1,29 +1,30 @@
 From: IPFire Project <no-reply@ipfire.org>
 To: {{ account.email_to }}
-Subject: {{ _("Time to get festive...") }}
+Subject: {{ _("Seasons Greetings!") }}
 Precedence: bulk
 X-Auto-Response-Suppress: OOF
 
-{{ _("Dear %s,") % account.first_name }}
+{{ _("Hi %s,") % account.first_name }}
 
-With the holiday season upon us, we hope you’re enjoying the festive treats
-and magical feeling that comes with this time of year.
+The year has drawn to an end and we just wanted to take a moment to say thank you.
+Your support for the IPFire project is hugely appreciated and we’re grateful that you’ve
+taken the time to be part of our ever-growing community.
 
-While you’re enjoying the festivities, it might be time to spare a thought for
-your IPFire appliance - whether in a cupboard or under a desk or sitting cool
-as a cucumber in your comms room, she’s been working away all year to look after you!
+We’re celebrating our best year yet including the recent success of our developer summit
+where we made huge progress on the build system for IPFire 3. This isn’t all that happened
+in 2023 though! We had may contributors who helped us to release 10 updates to
+millions of users across every continent.
+Yes, even our penguin friends in the antarctic are using IPFire!
 
-We would love if you could help us with a donation towards the IPFire project
-to keep our infrastructure running so your hardworking appliance can continue
-to keep you safe!
+We couldn’t have done this without hundreds of donations just like yours. If you’re feeling
+the love and you can spare a little money, help us start 2024 with a bang!
 
- https://www.ipfire.org/donate
 https://www.ipfire.org/donate
 
-Always remember to show appreciation to your IPFire appliance.
-Remember to keep her updated with the latest firmware and please don’t share your
-festive treats with her - most firewalls don’t enjoy pastry crumbs!
+Finally, whatever you’re celebrating at the end of this year, we wish you all the best
+for the festive season.
 
-{{ _("All the best,") }}
+{{ _("Thank you,") }}
 -{{ _("Your IPFire Team") }}
 
 --
index 5f070827c7591647c9342f54fd508d56ecd6578a..20455d31c6813f6160f929807c9e2630bac5df0b 100644 (file)
@@ -1,60 +1,46 @@
 {% extends "../../messages/base-promo.html" %}
 
 {% block content %}
-    <p>
-        <strong>{{ _("Dear Friends of IPFire,") }}</strong>
-    </p>
-
-       <p>
-               I’m writing to you, about to sign off for Christmas, filled with pride for our
-               great project and gratitude for our wonderful supporters.
-       </p>
-
-       <p>
-               This has been a challenging but enjoyable year and I’d like to thank you for
-               the support you’ve shown, whether through your contributions to the project,
-               your feedback or your donations, our project wouldn’t have such an impact
-               without people like you!
-       </p>
-
-       <p>
-               As ever, thank you to my fellow IPFire team members and contributors for your
-               efforts to make our project the best it can be
-                Everyone has worked super-hard this year so whatever you’re celebrating this
-                December, I wish the very best for you and your families over the festive
-                break and may you have a happy and prosperous New Year.
-       </p>
-
-       <p>
-               If you haven’t yet donated to the project, it isn’t too late -
-               you can donate via the link below.
-       </p>
-
-       <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
-               <tbody>
-                       <tr>
-                               <td align="left">
-                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                               <tbody>
-                                                       <tr>
-                                                               <td>
-                                    <a href="https://www.ipfire.org/donate" target="_blank">{{ _("Donate Now") }}</a>
-                                </td>
-                                                       </tr>
-                                               </tbody>
-                                       </table>
-                               </td>
-                       </tr>
-               </tbody>
-       </table>
-
-       <p>
-               Thank you again for your support - I wish you all a happy and relaxing end to
-               the year and I’ll look forward to hearing from you all in 2023.
-       </p>
-
-    <p>
-        {{ _("Best wishes and peace,") }}
-        <br>{{ _("-Michael") }}
-    </p>
+       <tr class="section">
+               <td>
+                       <h1>{{ _("Hi %s,") % account.first_name }}</h1>
+
+                       <p>
+                               Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+                               Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+                               LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+                               stories sent in by our friends and colleagues using IPFire.
+                       </p>
+
+                       <p>
+                               I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+                               in advance for your continued support as we head into 2024. As you know, we’re the only
+                               remaining truly Open Source Linux-based firewall and it is important to us not just to
+                               continue to develop this great project, but also to remain Open Source and available to all.
+                               Our mission is to continue to provide the same level of service with continued enhancements
+                               and better usability and all of this requires the continued to support of our fantastic users.
+                       </p>
+
+                       <p>
+                               If you’d like to help us kick off 2024 the right way, please click the link where
+                               you can donate to our project. Every donation is appreciated as it contributes to the
+                               continued success of our project and helps us keep our software open to all.
+                       </p>
+
+                       <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                               <tr class="button">
+                                       <td>
+                                               <a class="primary" href="https://www.ipfire.org/donate">
+                                                       {{ _("Donate") }}
+                                               </a>
+                                       </td>
+                               </tr>
+                       </table>
+
+                       <p>
+                               {{ _("Happy New Year!")}}
+                               <br>{{ _("-Michael") }}
+                       </p>
+               </td>
+       </tr>
 {% end block %}
index a57f962b5935a466eb3c357d0f6569574c8047bb..1a44c4a16574a46e4f5b906c0a92473333fbfe33 100644 (file)
@@ -1,34 +1,30 @@
 From: Michael Tremer <michael.tremer@ipfire.org>
 To: {{ account.email_to }}
-Subject: {{ _("Merry Christmas!") }}
+Subject: {{ _("Wishing You a Happy and Prosperous New Year") }}
 Precedence: bulk
 X-Auto-Response-Suppress: OOF
 
-Dear Friends of IPFire,
+{{ _("Hi %s,") % account.first_name }}
 
-I’m writing to you, about to sign off for Christmas, filled with pride for our
-great project and gratitude for our wonderful supporters.
+Did you see the highlights of 2023 we shared on our socials? If you didn’t, check our our
+Mastodon (https://social.ipfire.org/@news), Twitter (https://twitter.com/ipfire) and our
+LinkedIn (https://www.linkedin.com/company/ipfire) to see some of our favourite users
+stories sent in by our friends and colleagues using IPFire.
 
-This has been a challenging but enjoyable year and I’d like to thank you for
-the support you’ve shown, whether through your contributions to the project,
-your feedback or your donations, our project wouldn’t have such an impact
-without people like you!
+I wanted to take the opportunity to thank you for all of your support in 2023 and thank you
+in advance for your continued support as we head into 2024. As you know, we’re the only
+remaining truly Open Source Linux-based firewall and it is important to us not just to
+continue to develop this great project, but also to remain Open Source and available to all.
+Our mission is to continue to provide the same level of service with continued enhancements
+and better usability and all of this requires the continued to support of our fantastic users.
 
-As ever, thank you to my fellow IPFire team members and contributors for your
-efforts to make our project the best it can be
-Everyone has worked super-hard this year so whatever you’re celebrating this
-December, I wish the very best for you and your families over the festive
-break and may you have a happy and prosperous New Year.
-
-If you haven’t yet donated to the project, it isn’t too late -
-you can donate via the link below.
+If you’d like to help us kick off 2024 the right way, please click the link where
+you can donate to our project. Every donation is appreciated as it contributes to the
+continued success of our project and helps us keep our software open to all.
 
   https://www.ipfire.org/donate
 
-Thank you again for your support - I wish you all a happy and relaxing end to
-the year and I’ll look forward to hearing from you all in 2023.
-
-{{ _("Best wishes and peace,") }}
+{{ _("Happy New Year!")}}
 {{ _("-Michael") }}
 
 --
index 8a8a32f4aa8481a6aaa4f5129b61a6f240f357dc..4a5256810a4db9df8326fafcd1eb114f51db6d4e 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary is-fullheight-with-navbar">
                <div class="hero-body">
                        <div class="container">
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("Thank You") }}
                                </h1>
                                <p class="subtitle">
index 27d6930f3f727792e0074d9d4a6a1cf81b0c7e14..691d8e0d78741c58c8d903f4213eb1e10a281037 100644 (file)
@@ -9,7 +9,7 @@
        <section class="hero is-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -22,7 +22,7 @@
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("Mirrors") }}
                                </h1>
                                <p class="subtitle">
index d171cb0988bee5151e23893f1c86aa40a605ed39..ad1ee257bec0ae6fdc7d699cd7170d3f2785239f 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("Download %s") % release }}
                                </h1>
 
-                               <h6 class="subtitle is-6">
+                               <h6 class="subtitle">
                                        {{ _("Released %s") % locale.format_date(release.published, relative=True, shorter=True) }}
 
                                        {% if release.blog %}
index ff8dce87e6f2162c13d77c6486ec864e19b15892..ad490f8fe54e7fd12f7b1db078aa4f4d63ca2fb0 100644 (file)
@@ -6,10 +6,10 @@
        <section class="hero is-medium">
                <div class="hero-body">
                        <div class="container">
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        Thank You For Downloading IPFire<span class="has-text-primary">_</span>
                                </h1>
-                               <h4 class="subtitle is-4">{{ _("Your download will begin in a few seconds. If not, click the link below.") }}</h4>
+                               <h4 class="subtitle">{{ _("Your download will begin in a few seconds. If not, click the link below.") }}</h4>
 
                                <div class="block">
                                        <p class="download-path"></p>
index 4015b7dd8bcc4472439f8f6e53303bfb9cf94d60..bc2676c011044355f7e24fa254cdf10bcb70da41 100644 (file)
                                                </li>
                                        </ul>
                                </nav>
+                       </div>
+               </div>
+       </section>
 
-                               <section class="section">
-                                       <div class="columns is-vcentered">
-                                               <div class="column">
-                                                       <div class="has-text-centered">
-                                                               <h1 class="title is-1">{{ "{:,d}".format(total) }}</h1>
-                                                               <h4 class="title is-4">{{ _("Total amount of profiles") }}</h4>
-                                                       </div>
-                                               </div>
+       <section class="section">
+               <div class="container">
+                       <div class="columns">
+                               <div class="column">
+                                       <div class="has-text-centered">
+                                               <h1 class="title">{{ "{:,d}".format(total) }}</h1>
+                                               <h4 class="title is-4">{{ _("Total Profiles") }}</h4>
+                                       </div>
+                               </div>
 
-                                               <div class="column">
-                                                       <div class="has-text-centered">
-                                                               <h1 class="title is-1">{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
-                                                               <h4 class="title is-4">{{ _("Reporting back to us") }}</h4>
-                                                       </div>
-                                               </div>
+                               <div class="column">
+                                       <div class="has-text-centered">
+                                               <h1 class="title">{{ "%.2f%%" % (with_data * 100 / total) }}</h1>
+                                               <h4 class="title is-4">{{ _("Reporting back to us") }}</h4>
                                        </div>
-                               </section>
+                               </div>
                        </div>
                </div>
        </section>
+
+       {% if histogram %}
+               <section class="section">
+                       <div class="container">
+                               <table class="table">
+                                       <tr>
+                                               <th>{{ _("Date") }}</th>
+                                               <th>{{ _("Total Profiles") }}</th>
+                                       </tr>
+
+                                       {% for date in sorted(histogram, reverse=True) %}
+                                               <tr>
+                                                       <th scope="row">{{ format_date(date) }}</th>
+                                                       <td>
+                                                               {{ histogram[date] }}
+                                                       </td>
+                                               </tr>
+                                       {% end %}
+                               </table>
+                       </div>
+               </section>
+       {% end %}
 {% end block %}
index 6daba7f4f48d7e2637a16f3492a6db3673a3f7be..9013a8207bdbcd62f282228f1cc29b54e091f78e 100644 (file)
                                                </li>
                                        </ul>
                                </nav>
+                       </div>
+               </div>
+       </section>
 
+       <section class="section">
+               <div class="container">
+                       <h1 class="title">{{ driver }}</h1>
                                <h2 class="title is-2">{{ _("All known devices run by %s") % driver }}</h2>
                        </div>
                </div>
@@ -38,7 +44,7 @@
 
        <section class="section">
                <div class="container">
-                       {% module FireinfoDeviceTable(driver_map) %}
+                       {% module FireinfoDeviceTable(devices) %}
                </div>
        </section>
 {% end block %}
index 72d3ac9a9bbd2f10f17729f2059f7f86081101ea..9a67dffa6ba53978e23b7332ae73e3d81d2689be 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-medium is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="https://ipfire.org/">
@@ -21,7 +21,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Fireinfo") }}</h1>
+                               <h1 class="title">{{ _("Fireinfo") }}</h1>
 
                                <a class="button is-dark is-medium" href="/profile/random">
                                        {{ _("Show a Random Profile") }}
                <div class="container">
                        <div class="columns is-vcentered">
                                {% if latest_release %}
+                                       {% set usage = latest_release.get_usage(when=when) %}
+
                                        <div class="column is-half is-centered">
                                                <div>
-                                                       <h1 class="title is-1 has-text-primary">
-                                                               {{ "%.2f%%" % (latest_release.penetration * 100) }}
+                                                       <h1 class="title has-text-primary">
+                                                               {{ "%.2f%%" % (usage * 100) }}
                                                        </h1>
 
                                                        <h5 class="title is-5">
                <div class="container">
                        <h4 class="title is-4">{{ _("Locations") }}</h4>
 
-                       {% for country_code, percentage in locations %}
+                       {% for cc in sorted(locations, key=lambda cc: locations[cc], reverse=True) %}
                                <div class="columns is-mobile">
-                                       {% if percentage >= 0.01 %}
+                                       {% if locations[cc] >= 0.01 %}
                                                <div class="column is-one-fifth">
-                                                       <span class="flag-icon flag-icon-{{ country_code.lower() }}"></span>
-                                                       <span class="">{{ format_country_name(country_code) }}</span>
+                                                       <span class="flag-icon flag-icon-{{ cc.lower() }}"></span>
+                                                       <span class="">{{ format_country_name(cc) }}</span>
                                                </div>
 
                                                <div class="column is-7">
-                                                       {% module ProgressBar(percentage, "success") %}
+                                                       {% module ProgressBar(locations[cc], "success") %}
                                                </div>
                                        {% end %}
                                </div>
                        {% end %}
 
-                       <p>
-                               <span class="has-text-weight-bold">IPFire<span class="has-text-primary">_</span></span>
-                               {{_("is also running in these countries: %s") % locale.list([(format_country_name(c) or c)  for c, p in locations if p < 0.01]) }}
-                       </p>
+                       {% set other_countries = [cc for cc in locations if locations[cc] < 0.01] %}
+
+                       {% if other_countries %}
+                               <p>
+                                       <span class="has-text-weight-bold">IPFire<span class="has-text-primary">_</span></span>
+                                       {{_("is also running in these countries: %s") % locale.list([(format_country_name(cc) or cc) for cc in other_countries]) }}
+                               </p>
+                       {% end %}
                </div>
        </section>
 
                                        <div class="block">
                                                <h4 class="title is-4">{{ _("CPU Vendors") }}</h4>
 
-                                               {% for name, percentage in cpu_vendors %}
+                                               {% for vendor in sorted(cpu_vendors, key=lambda v: cpu_vendors[v], reverse=True) %}
                                                        <div class="columns">
-                                                               <div class="column is-1">{{ name }}</div>
+                                                               <div class="column is-1">{{ vendor }}</div>
 
                                                                <div class="column">
-                                                                       {% module ProgressBar(percentage, "success") %}
+                                                                       {% module ProgressBar(cpu_vendors[vendor], "success") %}
                                                                </div>
                                                        </div>
                                                {% end %}
                                        <div class="block">
                                                <h4 class="title is-4">{{ _("Architectures") }}</h4>
 
-                                               {% for name, percentage in arches %}
+                                               {% for arch in sorted(arches, key=lambda a: arches[a], reverse=True) %}
                                                        <div class="columns">
-                                                               <div class="column is-1">{{ name }}</div>
+                                                               <div class="column is-1">{{ arch }}</div>
 
                                                                <div class="column">
-                                                                       {% module ProgressBar(percentage, "success") %}
+                                                                       {% module ProgressBar(arches[arch], "success") %}
                                                                </div>
                                                        </div>
                                                {% end %}
                                        </div>
                                </div>
                                <div class="column is-half has-text-centered">
-                                               <h1 class="title is-1">{{ format_size(memory_avg * 1024, "MB") }}</h1>
+                                               <h1 class="title">{{ format_size(memory_avg * 1024, "MB") }}</h1>
 
                                                <span class="tag">
                                                                {{ _("Average Amount of Memory") }}
                <div class="container">
                        <div class="columns is-vcentered">
                                <div class="column is-half has-text-centered">
-                                       <h1 class="title is-1 has-text-primary">
+                                       <h1 class="title has-text-primary">
                                                {{ "%.2f%%" % (virtual_ratio * 100) }}
                                        </h1>
 
                                <div class="column is-half">
                                        <h4 class="title is-4">{{ _("Top Hypervisors") }}</h4>
 
-                                       {% for name, percentage in hypervisors %}
+                                       {% for vendor in sorted(hypervisors, key=lambda v: hypervisors[v], reverse=True) %}
                                                <div class="columns">
-                                                       {% if percentage >= 0.01 %}
+                                                       {% if hypervisors[vendor] >= 0.01 %}
                                                                <div class="column is-1">
-                                                                       {% if name == "unknown" %}
+                                                                       {% if vendor == "unknown" %}
                                                                                <span class="text-muted">{{ _("Unknown") }}</span>
-                                                                       {% elif name == "VMWare" %}
+                                                                       {% elif vendor == "VMWare" %}
                                                                                VMware
                                                                        {% else %}
-                                                                               {{ name }}
+                                                                               {{ vendor }}
                                                                        {% end %}
                                                                </div>
 
                                                                <div class="column is-8">
-                                                                       {% module ProgressBar(percentage, "success") %}
+                                                                       {% module ProgressBar(hypervisors[vendor], "success") %}
                                                                </div>
                                                        {% end %}
                                                </div>
index 62b496c097a9cd3875006e2208519594afbda244..e8528f3ec1f61aa7d4d284628972bea9dba9d1e4 100644 (file)
@@ -3,10 +3,12 @@
 {% block title %}{{ _("Processors") }}{% end block %}
 
 {% block container %}
+       {% set map = backend.fireinfo.get_cpu_flags_map(when=when) %}
+
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="https://ipfire.org/">
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Processors") }}</h1>
+                               <h1 class="title">{{ _("Processors") }}</h1>
                        </div>
                </div>
        </section>
 
        <section class="section">
                <div class="container">
-                       {% for platform in flags %}
-                               <h2 class="title is-2">{{ platform }}</h2>
+                       {% for arch in sorted(map) %}
+                               <h2 class="title is-2">{{ arch }}</h2>
 
                                {% for flag, percentage in flags[platform] %}
                                        <div class="columns">
                                                </div>
 
                                                <div class="column">
-                                                       {% if percentage >= 0.95 %}
-                                                               {% module ProgressBar(percentage, "success") %}
-                                                       {% elif percentage >= 0.5 %}
-                                                               {% module ProgressBar(percentage, "warning") %}
-                                                       {% elif percentage >= 0.1 %}
-                                                               {% module ProgressBar(percentage, "info") %}
+                                                       {% if p >= 0.95 %}
+                                                               {% module ProgressBar(p, "success") %}
+                                                       {% elif p >= 0.5 %}
+                                                               {% module ProgressBar(p, "warning") %}
+                                                       {% elif p >= 0.1 %}
+                                                               {% module ProgressBar(p, "info") %}
                                                        {% else %}
-                                                               {% module ProgressBar(percentage, "danger") %}
+                                                               {% module ProgressBar(p, "danger") %}
                                                        {% end %}
                                                </div>
                                        </div>
index c984294de0bf80084d30a8761cfbedca44c71823..6c0da12ecef72780d31942c68da36d9966312c84 100644 (file)
@@ -7,7 +7,7 @@
                <div class="container">
                        <div class="columns">
                                <div class="column is-8">
-                                       <h1 class="title is-1">{{ _("Profile") }}</h1>
+                                       <h1 class="title">{{ _("Profile") }}</h1>
                                        <h5 class="title is-5">{{ profile.public_id }}</h5>
                                </div>
                        </div>
        </section>
 
        <div class="container">
-               <div class="columns">
-                       <div class="column is-10">
-                               {% if profile.appliance_id %}
-                                       <div class="columns">
-                                               <div class="column is-7">
-                                                       <small>{{ _("This is a") }}</small>
-                                                       <h5 class="title is-5 is-lwl">{{ profile.appliance }}</h5>
-                                               </div>
-
-                                               <div class="column is-5">
-                                                       <a class="button is-lwl" href="https://www.lightningwirelabs.com">
-                                                               {{ _("Go to Lightning Wire Labs") }} <span class="fas fa-external-link-alt ml-2"></span>
-                                                       </a>
-                                               </div>
-                                       </div>
-                               {% end %}
-                       </div>
-               </div>
-
-
                                <div class="card mb-5">
                                        <div class="card-body">
                                                <div class="row">
                                                        <div class="col-12 col-sm-8 mb-4">
                                                                <h5 class="card-title mb-0">
-                                                                       {{ _("Running %s") % profile.release }}
+                                                                       {{ _("Running %s") % profile.system.release }}
                                                                </h5>
 
                                                                <small class="text-muted">
-                                                                       {{ _("Last update %s") % locale.format_date(profile.time_updated) }}
+                                                                       {{ _("Last update %s") % locale.format_date(profile.last_updated_at) }}
                                                                </small>
                                                        </div>
 
@@ -64,7 +44,7 @@
                                                </div>
 
                                                <dl class="row mb-0">
-                                                       {% if profile.virtual %}
+                                                       {% if profile.is_virtual() %}
                                                                <dt class="col-sm-3">{{ _("Hypervisor") }}</dt>
                                                                <dd class="col-sm-9">
                                                                        {% if profile.hypervisor == "VMWare" %}
                                                                                {{ profile.hypervisor }}
                                                                        {% end %}
                                                                </dd>
-                                                       {% elif not profile.appliance_id and profile.system %}
+                                                       {% elif profile.system %}
                                                                <dt class="col-sm-3">{{ _("System") }}</dt>
                                                                <dd class="col-sm-9">
-                                                                       {% if profile.system_vendor %}
-                                                                               {{ profile.system_vendor }}
+                                                                       {% if profile.system.vendor %}
+                                                                               {{ profile.system.vendor }}
                                                                        {% end %}
 
-                                                                       {% if profile.system_vendor and profile.system_model %}
+                                                                       {% if profile.system.vendor and profile.system.model %}
                                                                                &dash;
                                                                        {% end %}
 
-                                                                       {% if profile.system_model %}
-                                                                               {{ profile.system_model }}
+                                                                       {% if profile.system.model %}
+                                                                               {{ profile.system.model }}
                                                                        {% end %}
                                                                </dd>
                                                        {% end %}
                                                                </dd>
                                                        {% end %}
 
-                                                       {% if profile.storage %}
+                                                       {% if profile.system.storage %}
                                                                <dt class="col-md-3">{{ _("Storage") }}</dt>
                                                                <dd class="col-md-9">
-                                                                       {{ format_size(profile.storage) }}
+                                                                       {{ format_size(profile.system.storage) }}
                                                                </dd>
                                                        {% end %}
 
                                                                </dd>
                                                        {% end %}
 
-                                                       {% if profile.language %}
+                                                       {% if profile.system.language %}
                                                                <dt class="col-md-3">{{ _("Language") }}</dt>
                                                                <dd class="col-md-9">
-                                                                       {{ format_language_name(profile.language) }}
+                                                                       {{ format_language_name(profile.system.language) }}
                                                                </dd>
                                                        {% end %}
                                                </dl>
index 243878afac3b12f904dbe672d3f40ae97b47164d..88a884ddc42a645c43d4977051063d6bfa4362d2 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="https://ipfire.org/">
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Releases") }}</h1>
+                               <h1 class="title">{{ _("Releases") }}</h1>
                        </div>
                </div>
        </section>
 
        <section class="section">
                <div class="container">
-                       {% for name, percentage in releases %}
+                       {% for name in sorted(releases, key=lambda n: releases[n], reverse=True) %}
                                <div class="columns">
                                        <div class="column is-4">{{ name.replace("core", "Core Update ") }}</div>
 
                                        <div class="column">
-                                               {% module ProgressBar(percentage, "primary") %}
+                                               {% module ProgressBar(releases[name], "primary") %}
                                        </div>
                                </div>
                        {% end %}
 
                        <h2 class="title is-2">{{ _("Kernels") }}</h2>
 
-                       {% for name, percentage in kernels %}
+                       {% for name in sorted(kernels, key=lambda n: kernels[n], reverse=True) %}
                                <div class="columns">
                                        <div class="column is-4">{{ name }}</div>
 
                                        <div class="column">
-                                               {% module ProgressBar(percentage, "info") %}
+                                               {% module ProgressBar(kernels[name], "info") %}
                                        </div>
                                </div>
                        {% end %}
index fe5718a667e2845b11d6df40cabd44bced29829c..1dac1470565c4cdc3c409cea58b76ef627148cf1 100644 (file)
@@ -3,48 +3,48 @@
 {% block title %}{{ _("Vendors") }}{% end block %}
 
 {% block container %}
-<section class="hero is-primary">
-       <div class="hero-body">
-               <div class="container">
-                       <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
-                               <ul>
-                                       <li>
-                                               <a href="https://ipfire.org/">
-                                                       Home
-                                               </a>
-                                       </li>
-                                       <li>
-                                               <a href="/">
-                                                       {{ _("Fireinfo") }}
-                                               </a>
-                                       </li>
-                                       <li class="is-active">
-                                               <a href="#">
-                                                       {{ _("Vendors") }}
-                                               </a>
-                                       </li>
-                               </ul>
-                       </nav>
+       <section class="hero is-primary">
+               <div class="hero-body">
+                       <div class="container">
+                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                                       <ul>
+                                               <li>
+                                                       <a href="https://ipfire.org/">
+                                                               Home
+                                                       </a>
+                                               </li>
+                                               <li>
+                                                       <a href="/">
+                                                               {{ _("Fireinfo") }}
+                                                       </a>
+                                               </li>
+                                               <li class="is-active">
+                                                       <a href="#">
+                                                               {{ _("Vendors") }}
+                                                       </a>
+                                               </li>
+                                       </ul>
+                               </nav>
 
-                       <h1 class="title is-1">{{ _("Vendors") }}</h1>
+                               <h1 class="title is-1">{{ _("Vendors") }}</h1>
+                       </div>
                </div>
-       </div>
-</section>
+       </section>
 
-<section class="section">
-       <div class="container">
-               {% for vendor, subsystems in vendors %}
-                       <div class="columns">
-                               {% if vendor %}
-                                       <div class="column is-4">{{ vendor }}</div>
-                                       <div class="column is-3">
-                                               {% for subsystem, vendor_id in sorted(subsystems) %}
-                                                       <a href="/vendors/{{ subsystem }}/{{ vendor_id }}">{{ subsystem.upper() }}</a>
-                                               {% end %}
-                                       </div>
-                               {% end %}
-                       </div>
-               {% end %}
-       </div>
-</section>
+       <section class="section">
+               <div class="container">
+                       {% for vendor, subsystems in vendors %}
+                               <div class="columns">
+                                       {% if vendor %}
+                                               <div class="column is-4">{{ vendor }}</div>
+                                               <div class="column is-3">
+                                                       {% for subsystem, vendor_id in sorted(subsystems) %}
+                                                               <a href="/vendors/{{ subsystem }}/{{ vendor_id }}">{{ subsystem.upper() }}</a>
+                                                       {% end %}
+                                               </div>
+                                       {% end %}
+                               </div>
+                       {% end %}
+               </div>
+       </section>
 {% end block %}
index 78f703153f598d51b399f81530879f2d1b4eaa33..af79e31acef808e44c4b112ce76471787594ed63 100644 (file)
@@ -46,7 +46,7 @@
 
                <div class="hero-body">
                        <div class="container">
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("More Than A Firewall") }}
                                </h1>
 
index 25b10f9d520ac63395b0ead919d148e76c47e2cc..f826d25add5071303a1a414fb921e31fde2c7946 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -29,8 +29,8 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Download IPFire Location") }}</h1>
-                               <h6 class="subtitle is-6">
+                               <h1 class="title">{{ _("Download IPFire Location") }}</h1>
+                               <h6 class="subtitle">
                                        Learn how to download and install <code>libloc</code>
                                </h6>
                        </div>
index 07a5f8dce8bb4b64e53ffc56edf90c1418e60c5e..ddc75beeb9791338ccc20dbfc64a47435694c340 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -29,8 +29,8 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("IPFire Location") }}</h1>
-                               <h6 class="subtitle is-6">
+                               <h1 class="title">{{ _("IPFire Location") }}</h1>
+                               <h6 class="subtitle">
                                        <code>libloc</code> is versatile, fast and easy to use
                                        in any application.
                                </h6>
index 7db228870a2e703c81c509c61145f2c355896f22..6b62671f8d6773e7a244d081dfe60c6d6add0dc6 100644 (file)
@@ -10,7 +10,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -28,8 +28,8 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("IPFire Location") }}</h1>
-                               <h6 class="subtitle is-6">
+                               <h1 class="title">{{ _("IPFire Location") }}</h1>
+                               <h6 class="subtitle">
                                        {{ _("A powerful, free IP address location database") }}
                                </h6>
                        </div>
index 1e49e4fce6a30ad1e7894c8f2eee46d43ed6b972..27d64b9463f6703f9c233ac3a6e4973b3ac3fed2 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -29,7 +29,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Lookup %s") % address }}</h1>
+                               <h1 class="title">{{ _("Lookup %s") % address }}</h1>
                        </div>
                </div>
        </section>
index 2c56f343ce7d94569798fc4d9348bf8afc0995b4..7a95b02743b019f20024aef6f97eb0cc9f1b5058 100644 (file)
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
 
 {% block footer %}
-       {{ _("Don't like these emails?") }}
-       <a href="https://people.ipfire.org/unsubscribe">{{ _("Unsubscribe") }}</a>.
+       <unsubscribe>
+               <a href="https://www.ipfire.org/unsubscribe">{{ _("Unsubscribe") }}</a>
+       </unsubscribe>
 {% end block %}
index 421225d128b1e54b75684da1b8ff51c09089b2d1..145b5d10dcb529b0c4338237a7e74d69a4f145c2 100644 (file)
 <!DOCTYPE html>
-<html>
-       <head>
-               <meta name="viewport" content="width=device-width">
-               <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-               <title>{% block title %}{% end block %}</title>
-               <style media="all" type="text/css">
-                       {% include "main.css" %}
+<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+       {# Based on https://www.cerberusemail.com #}
+    <meta charset="utf-8">
+
+       {# Enable "responsiveness" #}
+    <meta name="viewport" content="width=device-width">
+
+       {# Use the latest (edge) version of IE rendering engine #}
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+       {# Disable auto-scale in iOS 10 Mail entirely #}
+    <meta name="x-apple-disable-message-reformatting">
+
+       {# Tell iOS not to automatically link certain text strings #}
+    <meta name="format-detection" content="telephone=no,address=no,email=no,date=no,url=no">
+
+       {# Declare supported color schemes #}
+    <meta name="color-scheme" content="light dark">
+    <meta name="supported-color-schemes" content="light dark">
+
+       {# Title #}
+    <title>{% block title %}{% end block %}</title>
+
+    {# Make background images in 72ppi Outlook render at correct size #}
+    <!--[if gte mso 9]>
+    <xml>
+        <o:OfficeDocumentSettings>
+            <o:PixelsPerInch>96</o:PixelsPerInch>
+        </o:OfficeDocumentSettings>
+    </xml>
+    <![endif]-->
+
+   {# Desktop Outlook chokes on web font references and defaults to Times New Roman, so we force a safe fallback font #}
+    <!--[if mso]>
+        <style>
+            * {
+                font-family: sans-serif !important;
+            }
+        </style>
+    <![endif]-->
+
+    {# All other clients get the webfont reference; some will render the font and others will silently fail to the fallbacks.
+               More on that here: https://web.archive.org/web/20190717120616/http://stylecampaign.com/blog/2015/02/webfont-support-in-email/ #}
+    <!--[if !mso]>
+               <style>
+                       {% include "fonts.css" %}
                </style>
-       </head>
-
-       <body>
-               <span class="preheader">{% block preheader %}{% end preheader %}</span>
-               <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
-                       <tr>
-                               <td class="container">
-                                       <div class="content">
-                                               {% block container %}
-                                                       <table role="presentation" class="main">
-                                                               <tr>
-                                                                       <td class="logo">
-                                                                               <img src="https://www.ipfire.org/static/img/ipfire-tux.png" alt="{{ _("IPFire Logo") }}">
-                                                                       </td>
-                                                               </tr>
-                                                               <tr>
-                                                                       <td class="wrapper">
-                                                                               <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                                                                       <tr>
-                                                                                               <td>
-                                                                                                       {% block content %}{% end block %}
-                                                                                               </td>
-                                                                                       </tr>
-                                                                               </table>
-                                                                       </td>
-                                                               </tr>
-                                                       </table>
-                                               {% end block %}
-
-                                               <div class="footer">
-                                                       <table role="presentation" border="0" cellpadding="0" cellspacing="0">
-                                                               <tr>
-                                                                       <td class="content-block">
-                                                                               <span class="apple-link">{{ _("The IPFire Project" )}}</span>
-
-                                                                               <br>
-
-                                                                               {% block footer %}{% end block %}
-                                                                       </td>
-                                                               </tr>
-                                                       </table>
-                                               </div>
-                                       </div>
+    <![endif]-->
+
+       {# Import the main CSS #}
+    <style>
+               {# Tell the email client that both light and dark styles are provided #}
+               :root {
+                       color-scheme: light dark
+                       supported-color-schemes: light dark
+               }
+
+               {% include "main.css" %}
+    </style>
+</head>
+
+<body class="bg">
+       <center role="article" aria-roledescription="email" lang="en" class="bg">
+               <!--[if mso | IE]>
+               <table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" class="bg">
+               <tr>
+               <td>
+               <![endif]-->
+                       {# Visually Hidden Pre-header Text #}
+                       <div class="pre-header" aria-hidden="true">
+                               {% block preview %}{% end block %}
+                       </div>
+
+                       {# Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview #}
+                       <div class="whitespace">
+                               &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;
+                       </div>
+
+                       <div class="container">
+                               <!--[if mso]>
+                               <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="600">
+                               <tr>
+                               <td>
+                               <![endif]-->
+
+                               {# Body #}
+                               <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                                       {% block body %}
+                                               {# Header #}
+                                               <tr class="header">
+                                                       <td>
+                                                               <h1>
+                                                                       IPFire<span class="has-text-primary">_</span>
+                                                               </h1>
+                                                       </td>
+                                               </tr>
+
+                                               {# Hero #}
+                                               {% block hero %}{% end block %}
+
+                                               {# Content #}
+                                               <tr class="content">
+                                                       <td>
+                                                               <table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
+                                                                       {% block content %}{% end block %}
+                                                               </tabke>
+                                                       </td>
+                                               </tr>
+                                       {% end block %}
+                               </table>
+
+                               {# Footer #}
+                               <table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0">
+                                       <tr class="footer">
+                                               <td>
+                                                       {{ _("The IPFire Project") }},
+                                                       {{ _("c/o") }} Lightning Wire Labs GmbH,
+                                                       <span class="unstyle-auto-detected-links">Gerhardstraße 8, 45711 Datteln, Germany</span>
+
+                                                       <br><br>
+
+                                                       {% block footer %}{% end block %}
+                                               </td>
+                                       </tr>
+                               </table>
+
+                               <!--[if mso]>
                                </td>
-                       </tr>
+                               </tr>
+                               </table>
+                               <![endif]-->
+                       </div>
+
+               <!--[if mso | IE]>
+               </td>
+               </tr>
                </table>
-       </body>
+               <![endif]-->
+    </center>
+</body>
 </html>
diff --git a/src/templates/messages/fonts.sass b/src/templates/messages/fonts.sass
new file mode 100644 (file)
index 0000000..368d054
--- /dev/null
@@ -0,0 +1,7 @@
+$baseurl: "https://michael.dev.ipfire.org"
+
+// Use our main font by default
+*
+       font-family: Prompt, sans-serif
+
+@import "../../sass/_fonts.sass"
index ad115765905a46fdc91726c6d162224e0cd4a5eb..bc0830ed73a6d87f0fba83e818b862abbf116ad4 100644 (file)
@@ -1 +1,267 @@
-// Needs to be re-done https://bugzilla.ipfire.org/show_bug.cgi?id=13050
+
+// Fonts
+$font-family:                                  Prompt, sans-serif
+
+$font-weight-normal:                   500
+$font-weight-bold:                             700
+
+// Container
+$container-width:                              600px
+
+// A unit to use for padding
+$pad:                                                  20px
+
+// Borders
+$border-radius:                                        4px
+
+// Colours
+$black:                                                        hsl(0, 0%, 4%)
+$white:                                                        hsl(0, 0%, 100%)
+$grey:                                                 hsl(0, 0%, 97%)
+$light:                                                        hsl(0, 0%, 80%)
+
+$primary:                                              #ff2e52
+$primary-inverted:                             $white
+
+// Background Colours
+$bg-light:                                             $white
+$bg-dark:                                              $grey
+
+// Text Colour
+$text:                                                 $black
+$link:                                                 $primary
+
+// Font sizes
+$font-size-small:                              12px
+$font-size-normal:                             16px
+$font-size-large:                              20px
+
+$line-height-small:                            16px
+$line-height-normal:                   22px
+$line-height-large:                            28px
+
+// Headings
+$title-1:                                              30px
+$line-height-title-1:                  40px
+
+// Remove spaces around the email design added by some email clients
+html, body
+       margin: 0 auto !important
+       padding: 0 !important
+       height: 100% !important
+       width: 100% !important
+
+// Stop email clients resizing small text
+*
+       -ms-text-size-adjust: 100%
+       -webkit-text-size-adjust: 100%
+
+// Centers email on Android 4.4
+div[style*="margin: 16px 0"]
+       margin: 0 !important
+
+// forces Samsung Android mail clients to use the entire viewport
+#MessageViewBody, #MessageWebViewDiv
+       width: 100% !important
+
+// Stop Outlook from adding extra spacing to tables
+table, td
+       mso-table-lspace: 0pt !important
+       mso-table-rspace: 0pt !important
+
+// Fix a webkit padding issue
+table
+       border-spacing: 0 !important
+       border-collapse: collapse !important
+       table-layout: fixed !important
+       margin: 0 auto !important
+
+// Use a better rendering method when resizing images in IE
+img
+       -ms-interpolation-mode: bicubic
+
+// Prevent Windows 10 Mail from underlining links despite inline CSS
+a
+       text-decoration: none
+
+// A work-around for email clients meddling in triggered links
+a[x-apple-data-detectors], .unstyle-auto-detected-links a, .aBn
+       border-bottom: 0 !important
+       cursor: default !important
+       color: inherit !important
+       text-decoration: none !important
+       font-size: inherit !important
+       font-family: inherit !important
+       font-weight: inherit !important
+       line-height: inherit !important
+
+// Prevent Gmail from displaying a download button on large, non-linked images
+.a6S
+       display: none !important
+       opacity: 0.01 !important
+
+// Prevent Gmail from changing the text color in conversation threads.
+.im
+       color: inherit !important
+
+// If the above doesn't work, add a .g-img class to any image in question.
+img.g-img + div
+       display: none !important
+
+// Set font
+*
+       font-family: $font-family
+       font-weight: $font-weight-normal
+
+body
+       mso-line-height-rule: exactly
+
+// Links
+a
+       color: $link
+
+       &:hover
+               text-decoration: underline
+
+// Center all content
+center
+       width: 100%
+
+// Visually Hidden Pre-header Text
+.pre-header
+       max-height: 0
+       overflow: hidden
+       mso-hide: all
+
+// Some whitespace
+.whitespace
+       display: none
+       font-size: 1px
+       line-height: 1px
+       max-height: 0px
+       max-width: 0px
+       opacity: 0
+       overflow: hidden
+       mso-hide: all
+
+// The main container
+.container
+       max-width: $container-width
+       margin: 0 auto
+
+       // Improve readability on small screens
+       @media screen and (max-width: 600px)
+               p
+                       font-size: 17px !important;
+
+// Make tables fill the entire viewport horizontally
+table
+       width: 100%
+       margin: auto
+
+       // The header box
+       tr.header
+               td
+                       padding: $pad 0
+                       text-align: center
+
+                       h1
+                               margin: 0 0 10px 0
+                               font-size: 50px
+                               line-height: 60px
+                               font-weight: $font-weight-bold
+
+                               span
+                                       color: $primary
+                                       font-weight: i$font-weight-bold
+
+       // The hero unit
+       tr.hero
+               td
+                       img
+                               display: block
+                               border: 0
+                               width: 100%
+                               max-width: $container-width
+                               height: auto
+                               background: $grey
+                               margin: auto
+
+       // Content (i.e. the big box)
+       tr.content
+               td
+                       background-color: $bg-dark
+                       color: $text
+
+                       @media (prefers-color-scheme: dark)
+                               background-color: $bg-light
+
+                       table
+                               // One block in the box
+                               tr.section
+                                       td
+                                               padding: $pad
+                                               font-size: $font-size-normal
+                                               line-height: $line-height-normal
+
+                                               // Headings
+                                               h1
+                                                       margin: 0 0 10px 0
+                                                       font-size: $title-1
+                                                       line-height: $line-height-title-1
+
+                                               // Text
+                                               p
+                                                       padding: 8px 0
+                                                       margin: 0
+
+                                                       &:last-child
+                                                               padding: 0
+
+                                               // Links
+                                               a
+                                                       color: $link
+
+                                                       &:hover
+                                                               text-decoration: underline
+
+                               // Buttons
+                               tr.button
+                                       td
+                                               a
+                                                       display: block
+                                                       border: 1px solid $primary
+                                                       border-radius: $border-radius
+                                                       text-align: center
+                                                       font-size: $font-size-large
+                                                       font-weight: $font-weight-bold
+                                                       line-height: $line-height-large
+                                                       text-decoration: none
+                                                       padding: 16px 20px
+                                                       color: $white
+
+                                                       &.primary
+                                                               background-color: $primary
+                                                               color: $primary-inverted
+
+                                                               &:hover
+                                                                       background-color: $primary-inverted
+                                                                       color: $primary
+
+                               // Change the padding on the last element
+                               //tr:last-child
+                               //      td
+                               //              padding: 0 $pad
+
+       // Footer
+       tr.footer
+               td
+                       padding: $pad
+                       font-size: $font-size-small
+                       line-height: $line-height-small
+                       color: $light
+                       text-align: center
+
+                       // Make links grey, too
+                       a
+                               color: inherit
diff --git a/src/templates/modules/ipfire-logo.html b/src/templates/modules/ipfire-logo.html
new file mode 100644 (file)
index 0000000..ac2d64c
--- /dev/null
@@ -0,0 +1,18 @@
+{% set pride_colors = ("pride-red", "pride-orange", "pride-yellow", "pride-green", "pride-blue", "pride-purple") %}
+
+{# Christmas #}
+{% if now.month == 12 %}
+       IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} ðŸŽ„
+
+{# Halloween #}
+{% elif now.month == 10 and now.day >= 28 %}
+       IPFire<span class="has-text-primary">_</span>{{ suffix or "" }} ðŸŽƒ
+
+{# Pride Month #}
+{% elif now.month == 7 %}
+       {% for color, i in zip(pride_colors, "IPFire") %}<span class="has-text-{{ color }}">{{ i }}</span>{% end %}_{{ suffix or "" }}
+
+{# Other times of the year #}
+{% else %}
+       IPFire<span class="has-text-primary">_</span>{{ suffix or "" }}
+{% end %}
index 155481309b2a511ddbd7817335bde081fec01609..6a6106cc6f16dcdc5f71060ab2ca16e4373da0bb 100644 (file)
@@ -12,7 +12,7 @@
        <section class="hero has-background-primary-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -22,7 +22,7 @@
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {% if mode == "paste" %}
                                                {{ _("New Paste") }}
                                        {% elif mode == "upload" %}
index 80e22075bd851bccfb95050204d4ed94ecc47575..ec3c05c5bccee0c40b218b9ffb00d7320bfe6013 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-primary">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        About IPFire<span class="has-text-primary">_</span>
                                </h1>
-                               <p class="subtitle">The Open Source Firewall</p>
+                               <h6 class="subtitle">The Open Source Firewall</h6>
                        </div>
                </div>
        </section>
index 0d1c5edc0ab73e4d03f72513c0d82ea1f618db18..9d773e58a2b60297529d9085894a2c6e91d07763 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero has-background-primary-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -17,7 +17,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">
+                               <h1 class="title">
                                        {{ _("Need Help?") }}
                                </h1>
 
index d15bccf7e9719d8a5547ab2a3e95186d59340829..20d2371270d5b6cba0bb2add456caec7e271b560 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero has-background-primary-light">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">Home</a>
@@ -16,7 +16,7 @@
                                                </li>
                                        </ul>
                                </nav>
-                               <h1 class="title is-1">{{ _("Legal") }}</h1>
+                               <h1 class="title">{{ _("Legal") }}</h1>
                        </div>
                </div>
        </section>
index 053513c2b4b9787b7ce920f2d60d4c2b5148ddbd..04653ce5a73c4d3a21ce50cc82e5bbca3dc8b31a 100644 (file)
                                                </p>
                                        </div>
                                </div>
+
+                               {% if current_user and current_user.is_staff() %}
+                                       <div class="column is-one-third has-text-centered">
+                                               <div>
+                                                       <p class="heading">Telephony</p>
+                                                       <p class="title">
+                                                               <a href="/voip">
+                                                                       {{ _("VoIP") }}
+                                                               </a>
+                                                       </p>
+                                               </div>
+                                       </div>
+                               {% end %}
                        </div>
                </div>
        </section>
index 9b0367a00624ddcae8989277f86ae16ac08363de..5681e69d3095a0bcbedb9bd1064786191f4da97d 100644 (file)
@@ -7,10 +7,10 @@
                <div class="container">
                        <div class="columns is-centered">
                                <div class="column is-one-third">
-                                       <h1 class="title is-1">
+                                       <h1 class="title">
                                                {{ _("Delete User") }}
                                        </h1>
-                                       <h4 class="subtitle is-4">{{ account }}</h4>
+                                       <h4 class="subtitle">{{ account }}</h4>
 
                                        <div class="block has-text-danger">
                                                <form action="" method="POST">
index 470d1be52de8e98fa1524750ef94608a9226ea03..7ea2d885d112db2f1beb51adfb7ac74a4f6677e0 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-dark">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -31,8 +31,8 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Edit Profile") }}</h1>
-                               <h6 class="subtitle is-6">{{ account }} | {{ account.uid }}</h6>
+                               <h1 class="title">{{ _("Edit Profile") }}</h1>
+                               <h6 class="subtitle">{{ account }} | {{ account.uid }}</h6>
                        </div>
                </div>
        </section>
index a8bf8c0efc5625c5d7cf7daccd52363cf995adbe..eb695fb81d0c69af81dadf8b9465c32a9ab744d1 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-dark">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -24,7 +24,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Groups") }}</h1>
+                               <h1 class="title">{{ _("Groups") }}</h1>
                        </div>
                </div>
        </section>
index 5e6225fe9aab127206df647a65dd6e7ad79501e3..f17a6397b8e7717b61d549d6d35cd544167a6df9 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-dark">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -29,8 +29,8 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ group }}</h1>
-                               <h6 class="subtitle is-6">{{ group.gid }}</h6>
+                               <h1 class="title">{{ group }}</h1>
+                               <h6 class="subtitle">{{ group.gid }}</h6>
                        </div>
                </div>
        </section>
index 6fba6d3d05c19c1f624546e588551229d2a8df0b..499a17287787d9c35f649c26119e87045d947086 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-dark">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -19,7 +19,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("Users") }}</h1>
+                               <h1 class="title">{{ _("Users") }}</h1>
                        </div>
                </div>
        </section>
index 33d9ea022b047a4076d22b293013434b0770ddb1..edfedbd2f0485fd4d9421f002abd24ad54f617a4 100644 (file)
@@ -10,7 +10,7 @@
                        <div class="container">
                                <div class="columns is-vcentered">
                                        <div class="column">
-                                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                                        <ul>
                                                                <li>
                                                                        <a href="/">
@@ -28,8 +28,8 @@
                                                        </ul>
                                                </nav>
 
-                                               <h1 class="title is-1">{{ account }}</h1>
-                                               <h6 class="subtitle is-6">{{ account.uid }}</h6>
+                                               <h1 class="title">{{ account }}</h1>
+                                               <h6 class="subtitle">{{ account.uid }}</h6>
 
                                                {# Description #}
                                                {% if account.description %}
                                {% end %}
 
                                {% if account.can_be_managed_by(current_user) %}
-                                       <a class="button is-light" href="/users/{{ account.uid }}/passwd">
-                                               {{ _("Change Password") }}
-                                       </a>
-
                                        <a class="button is-warning" href="/users/{{ account.uid }}/edit">
                                                {{ _("Edit") }}
                                        </a>
 
                                                <a href="/users/{{ account.uid }}/edit#description">{{ _("Edit Profile") }}</a>
                                        </div>
-
-                               {# Suggest uploading an avatar if this user does not have one #}
-                               {% elif not current_user.has_avatar() %}
-                                       <div class="notification is-info">
-                                               <strong>{{ _("Upload An Avatar!") }}</strong>
-
-                                               {{ _("A picture says more than a thousand words") }}
-
-                                               <a href="/users/{{ account.uid }}/edit#avatar">{{ _("Upload Avatar") }}</a>
-                                       </div>
                                {% end %}
                        {% end %}
                </div>
index 7177cb213e191bbe488afacafc4a10e3cfe2cc72..6e61d244969826e8eb8b2c44869f1097e70094da 100644 (file)
@@ -6,7 +6,7 @@
        <section class="hero is-dark">
                <div class="hero-body">
                        <div class="container">
-                               <nav class="breadcrumb is-medium" aria-label="breadcrumbs">
+                               <nav class="breadcrumb" aria-label="breadcrumbs">
                                        <ul>
                                                <li>
                                                        <a href="/">
@@ -19,7 +19,7 @@
                                        </ul>
                                </nav>
 
-                               <h1 class="title is-1">{{ _("VoIP") }}</h1>
+                               <h1 class="title">{{ _("VoIP") }}</h1>
                        </div>
                </div>
        </section>
index 39ef3e0fab339e0dc135008abbdc3d2689e53cc2..94dc506919c001289c8020b1aec0f315c79e58a8 100644 (file)
@@ -97,6 +97,7 @@ class Application(tornado.web.Application):
 
                                # Misc
                                "ChristmasBanner"      : ui_modules.ChristmasBannerModule,
+                               "IPFireLogo"           : ui_modules.IPFireLogoModule,
                                "Markdown"             : ui_modules.MarkdownModule,
                                "Map"                  : ui_modules.MapModule,
                                "ProgressBar"          : ui_modules.ProgressBarModule,
@@ -135,6 +136,7 @@ class Application(tornado.web.Application):
                        (r"/blog/([0-9a-z\-\._]+)/delete", blog.DeleteHandler),
                        (r"/blog/([0-9a-z\-\._]+)/edit", blog.EditHandler),
                        (r"/blog/([0-9a-z\-\._]+)/publish", blog.PublishHandler),
+                       (r"/blog/([0-9a-z\-\._]+)/debug/email", blog.DebugEmailHandler),
 
                        # Docs
                        (r"/docs/recent\-changes", docs.RecentChangesHandler),
@@ -143,13 +145,13 @@ class Application(tornado.web.Application):
                        (r"/docs/watchlist", docs.WatchlistHandler),
                        (r"/docs/_restore", docs.RestoreHandler),
                        (r"/docs/_upload", docs.UploadHandler),
-                       (r"/docs/([A-Za-z0-9\-_\/]+)?/_edit", docs.EditHandler),
-                       (r"/docs/([A-Za-z0-9\-_\/]+)?/_render", docs.RenderHandler),
-                       (r"/docs/([A-Za-z0-9\-_\/]+)?/_(watch|unwatch)", docs.WatchHandler),
+                       (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_edit", docs.EditHandler),
+                       (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_render", docs.RenderHandler),
+                       (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_(watch|unwatch)", docs.WatchHandler),
                        (r"/docs/((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))/_delete", docs.DeleteFileHandler),
                        (r"/docs((?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))$", docs.FileHandler),
-                       (r"/docs([A-Za-z0-9\-_\/]+)?/_files", docs.FilesHandler),
-                       (r"/docs([A-Za-z0-9\-_\/]+)?", docs.PageHandler),
+                       (r"/docs(?:/([A-Za-z0-9\-_\/]+))?/_files", docs.FilesHandler),
+                       (r"/docs(?:/([A-Za-z0-9\-_\/]+))?", docs.PageHandler),
 
                        # Downloads
                        (r"/downloads", downloads.IndexHandler),
index ad2175173a6287bf18b42a2f7e9db01dda5eef41..fa4193792aafdcfa96eb376be06b3e5031bbcca9 100644 (file)
@@ -1,5 +1,6 @@
 #!/usr/bin/python
 
+import asyncio
 import datetime
 import dateutil.parser
 import functools
@@ -14,23 +15,34 @@ from ..decorators import *
 from .. import util
 
 class ratelimit(object):
-       def __init__(self, minutes=15, requests=180):
-               self.minutes = minutes
+       """
+               A decorator class which limits how often a function can be called
+       """
+       def __init__(self, *, minutes, requests):
+               self.minutes  = minutes
                self.requests = requests
 
        def __call__(self, method):
                @functools.wraps(method)
-               def wrapper(handler, *args, **kwargs):
+               async def wrapper(handler, *args, **kwargs):
                        # Pass the request to the rate limiter and get a request object
                        req = handler.backend.ratelimiter.handle_request(handler.request,
                                handler, minutes=self.minutes, limit=self.requests)
 
                        # If the rate limit has been reached, we won't allow
                        # processing the request and therefore send HTTP error code 429.
-                       if req.is_ratelimited():
+                       if await req.is_ratelimited():
                                raise tornado.web.HTTPError(429, "Rate limit exceeded")
 
-                       return method(handler, *args, **kwargs)
+                       # Call the wrapped method
+                       result = method(handler, *args, **kwargs)
+
+                       # Await it if it is a coroutine
+                       if asyncio.iscoroutine(result):
+                               return await result
+
+                       # Return the result
+                       return result
 
                return wrapper
 
@@ -206,10 +218,6 @@ class BaseHandler(tornado.web.RequestHandler):
        def iuse(self):
                return self.backend.iuse
 
-       @property
-       def memcached(self):
-               return self.backend.memcache
-
        @property
        def mirrors(self):
                return self.backend.mirrors
index a2ac1e3285f48b7c27ff33195c6e1cd6e0e9e159..20d2d74d7f9782c88b0c2f3b560ce876dce6467f 100644 (file)
@@ -235,6 +235,20 @@ class DeleteHandler(base.BaseHandler):
                self.redirect("/drafts")
 
 
+class DebugEmailHandler(base.BaseHandler):
+       @tornado.web.authenticated
+       def get(self, slug):
+               if not self.current_user.is_staff():
+                       raise tornado.web.HTTPError(403)
+
+               # Fetch the post
+               post = self.backend.blog.get_by_slug(slug)
+               if not post:
+                       raise tornado.web.HTTPError(404, "Could not find post %s" % slug)
+
+               self.render("blog/messages/announcement.html", account=self.current_user, post=post)
+
+
 class HistoryNavigationModule(ui_modules.UIModule):
        def render(self):
                return self.render_string("blog/modules/history-navigation.html",
index c4b77ba30e7cec1bc61af5f67c48ec04ec44bf74..41fc6332f105b01066ba7afd01c1fc315b771164 100644 (file)
@@ -109,7 +109,7 @@ class FileHandler(base.BaseHandler):
        def action(self):
                return self.get_argument("action", None)
 
-       def get(self, path):
+       async def get(self, path):
                # Check permissions
                if not self.backend.wiki.check_acl(path, self.current_user):
                        raise tornado.web.HTTPError(403, "Access to %s not allowed for %s" % (path, self.current_user))
@@ -138,7 +138,7 @@ class FileHandler(base.BaseHandler):
 
                # Check if image should be resized
                if size and file.is_bitmap_image():
-                       blob = file.get_thumbnail(size)
+                       blob = await file.get_thumbnail(size)
                else:
                        blob = file.blob
 
index 26b0ad6bb3d926ac62f12a861c6ba766e3b15106..bae215745af82879887c0c510f22a932b4f9e427 100644 (file)
@@ -5,13 +5,21 @@ import tornado.web
 
 from . import base
 
+SKUS = {
+       "monthly"   : "IPFIRE-DONATION-MONTHLY",
+       "quarterly" : "IPFIRE-DONATION-QUARTERLY",
+       "yearly"    : "IPFIRE-DONATION-YEARLY",
+}
+DEFAULT_SKU = "IPFIRE-DONATION"
+
 class DonateHandler(base.BaseHandler):
        def get(self):
-               country = self.current_country_code
+               if self.current_user:
+                       country = self.current_user.country_code
+               else:
+                       country = self.current_country_code
 
                # Get defaults
-               first_name = self.get_argument("first_name", None)
-               last_name = self.get_argument("last_name", None)
                amount    = self.get_argument_float("amount", None)
                currency  = self.get_argument("currency", None)
                frequency = self.get_argument("frequency", None)
@@ -29,21 +37,35 @@ class DonateHandler(base.BaseHandler):
                        frequency = "one-time"
 
                self.render("donate/donate.html", countries=iso3166.countries,
-                       country=country, first_name=first_name, last_name=last_name,
-                       amount=amount, currency=currency, frequency=frequency)
+                       country=country, amount=amount, currency=currency, frequency=frequency)
 
        @base.ratelimit(minutes=15, requests=5)
        async def post(self):
+               type      = self.get_argument("type")
+               if not type in ("individual", "organization"):
+                       raise tornado.web.HTTPError(400, "type is of an invalid value: %s" % type)
+
                amount    = self.get_argument("amount")
                currency  = self.get_argument("currency", "EUR")
                frequency = self.get_argument("frequency")
 
-               # Collect donor information
-               donor = {
+               organization = None
+               locale = self.get_browser_locale()
+
+               # Get organization information
+               if type == "organization":
+                       organization = {
+                               "name"         : self.get_argument("organization"),
+                               "vat_number"   : self.get_argument("vat_number", None),
+                       }
+
+               # Collect person information
+               person = {
                        "email"        : self.get_argument("email"),
                        "title"        : self.get_argument("title"),
                        "first_name"   : self.get_argument("first_name"),
                        "last_name"    : self.get_argument("last_name"),
+                       "locale"       : locale.code,
                }
 
                # Collect address information
@@ -58,41 +80,30 @@ class DonateHandler(base.BaseHandler):
 
                # Send everything to Zeiterfassung
                try:
-                       # Search for person or create a new one
-                       response = await self.backend.zeiterfassung.send_request(
-                               "/api/v1/persons/search", **donor
-                       )
+                       # Create a new organization
+                       if organization:
+                               organization = await self._create_organization(organization, address)
 
-                       if not response:
-                               response = await self.backend.zeiterfassung.send_request(
-                                       "/api/v1/persons/create", **donor, **address
-                               )
-
-                       person = response.get("number")
+                       # Create a person
+                       person = await self._create_person(person, address, organization)
 
-                       donation = {
-                               "person"       : person,
+                       # Create a new order
+                       order = await self._create_order(person=person, currency=currency)
 
-                               # $$$
-                               "amount"       : amount,
-                               "currency"     : currency,
+                       # Add donation to the order
+                       await self._create_donation(order, frequency, amount, currency,
+                               vat_included=(type == "individual"))
 
-                               # Is this a recurring donation?
-                               "recurring"    : frequency == "monthly",
+                       # Submit the order
+                       needs_payment = await self._submit_order(order)
 
-                               # Add URLs to redirect the user back
-                               "success_url"  : "https://%s/donate/thank-you" % self.request.host,
-                               "error_url"    : "https://%s/donate/error" % self.request.host,
-                               "back_url"     : "https://%s/donate?amount=%s&currency=%s&frequency=%s" %
-                                       (self.request.host, amount, currency, frequency),
-                       }
-
-                       # Create donation
-                       response = await self.backend.zeiterfassung.send_request(
-                               "/api/v1/donations/create/ipfire-project", **donation, **address)
+                       # Pay the order
+                       if needs_payment:
+                               redirect_url = await self._pay_order(order)
+                       else:
+                               redirect_url = "https://%s/donate/thank-you" % self.request.host
 
                        # Redirect the user to the payment page
-                       redirect_url = response.get("redirect_url")
                        if not redirect_url:
                                raise tornado.web.HTTPError(500, "Did not receive a redirect URL")
 
@@ -102,6 +113,139 @@ class DonateHandler(base.BaseHandler):
                except Exception:
                        raise
 
+       async def _create_organization(self, organization, address):
+               # Check if we have an existing organization
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/organizations/search", **organization,
+               )
+
+               # Update details if we found a match
+               if response:
+                       number = response.get("number")
+
+                       # Update name
+                       await self.backend.zeiterfassung.send_request(
+                               "/api/v1/organizations/%s/name" % number, **organization
+                       )
+
+                       # Update VAT number
+                       vat_number = organization.get("vat_number", None)
+                       if vat_number:
+                               await self.backend.zeiterfassung.send_request(
+                                       "/api/v1/organizations/%s/vat-number" % number, vat_number=vat_number,
+                               )
+
+                       # Update address
+                       await self.backend.zeiterfassung.send_request(
+                               "/api/v1/organizations/%s/address" % number, **address,
+                       )
+
+                       return number
+
+               # Otherwise we will create a new one
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/organizations/create", **organization, **address,
+               )
+
+               # Return the organization's number
+               return response.get("number")
+
+       async def _create_person(self, person, address, organization=None):
+               """
+                       Searches for a matching person or creates a new one
+               """
+               # Check if we have an existing person
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/persons/search", **person
+               )
+
+               # Update details if we found a match
+               if response:
+                       number = response.get("number")
+
+                       # Update name
+                       await self.backend.zeiterfassung.send_request(
+                               "/api/v1/persons/%s/name" % number, **person,
+                       )
+
+                       # Update address
+                       await self.backend.zeiterfassung.send_request(
+                               "/api/v1/persons/%s/address" % number, **address,
+                       )
+
+                       return number
+
+               # If not, we will create a new one
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/persons/create", organization=organization, **person, **address
+               )
+
+               # Return the persons's number
+               return response.get("number")
+
+       async def _create_order(self, person, currency=None):
+               """
+                       Creates a new order and returns its ID
+               """
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/orders/create", person=person, currency=currency,
+               )
+
+               # Return the order number
+               return response.get("number")
+
+       async def _create_donation(self, order, frequency, amount, currency,
+                       vat_included=False):
+               """
+                       Creates a new donation
+               """
+               # Select the correct product
+               try:
+                       sku = SKUS[frequency]
+               except KeyError:
+                       sku = DEFAULT_SKU
+
+               # Add it to the order
+               await self.backend.zeiterfassung.send_request(
+                       "/api/v1/orders/%s/products/add" % order, sku=sku, quantity=1,
+               )
+
+               # Set the price
+               await self.backend.zeiterfassung.send_request(
+                       "/api/v1/orders/%s/products/%s/price" % (order, sku),
+                       price=amount, currency=currency, vat_included=vat_included,
+               )
+
+       async def _submit_order(self, order):
+               """
+                       Submits the order
+               """
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/orders/%s/submit" % order,
+               )
+
+               # Return whether this needs payment
+               return not response.get("is_authorized")
+
+       async def _pay_order(self, order):
+               """
+                       Pay the order
+               """
+               # Add URLs to redirect the user back
+               urls = {
+                       "success_url" : "https://%s/donate/thank-you" % self.request.host,
+                       "error_url"   : "https://%s/donate/error" % self.request.host,
+                       "back_url"    : "https://%s/donate" % self.request.host,
+               }
+
+               # Send request
+               response = await self.backend.zeiterfassung.send_request(
+                       "/api/v1/orders/%s/pay" % order, **urls,
+               )
+
+               # Return redirect URL
+               return response.get("redirect_url", None)
+
 
 class ThankYouHandler(base.BaseHandler):
     def get(self):
index 976b9e1d1fe3676b9310d56320fa2c4294dfc469..8b0f8e8e759287d86ac8b2865611b1d702c14909 100644 (file)
@@ -1,13 +1,8 @@
 #!/usr/bin/python
 
-import datetime
-import logging
-import re
 import json
 import tornado.web
 
-from .. import fireinfo
-
 from . import base
 from . import ui_modules
 
@@ -17,100 +12,11 @@ class BaseHandler(base.BaseHandler):
                return self.get_argument_date("when", None)
 
 
-MIN_PROFILE_VERSION = 0
-MAX_PROFILE_VERSION = 0
-
-class Profile(dict):
-       def __getattr__(self, key):
-               try:
-                       return self[key]
-               except KeyError:
-                       raise AttributeError(key)
-
-       def __setattr__(self, key, val):
-               self[key] = val
-
-
 class ProfileSendHandler(BaseHandler):
        def check_xsrf_cookie(self):
                # This cookie is not required here.
                pass
 
-       def prepare(self):
-               # Create an empty profile.
-               self.profile = Profile()
-
-       def __check_attributes(self, profile):
-               """
-                       Check for attributes that must be provided,
-               """
-               attributes = (
-                       "private_id",
-                       "profile_version",
-                       "public_id",
-               )
-               for attr in attributes:
-                       if attr not in profile:
-                               raise tornado.web.HTTPError(400, "Profile lacks '%s' attribute: %s" % (attr, profile))
-
-       def __check_valid_ids(self, profile):
-               """
-                       Check if IDs contain valid data.
-               """
-               for id in ("public_id", "private_id"):
-                       if re.match(r"^([a-f0-9]{40})$", "%s" % profile[id]) is None:
-                               raise tornado.web.HTTPError(400, "ID '%s' has wrong format: %s" % (id, profile))
-
-       def __check_equal_ids(self, profile):
-               """
-                       Check if public_id and private_id are equal.
-               """
-               if profile.public_id == profile.private_id:
-                       raise tornado.web.HTTPError(400, "Public and private IDs are equal: %s" % profile)
-
-       def __check_matching_ids(self, profile):
-               """
-                       Check if a profile with the given public_id is already in the
-                       database. If so we need to check if the private_id matches.
-               """
-               p = self.profiles.find_one({ "public_id" : profile["public_id"]})
-               if not p:
-                       return
-
-               p = Profile(p)
-               if p.private_id != profile.private_id:
-                       raise tornado.web.HTTPError(400, "Mismatch of private_id: %s" % profile)
-
-       def __check_profile_version(self, profile):
-               """
-                       Check if this version of the server software does support the
-                       received profile.
-               """
-               version = profile.profile_version
-
-               if version < MIN_PROFILE_VERSION or version > MAX_PROFILE_VERSION:
-                       raise tornado.web.HTTPError(400,
-                               "Profile version is not supported: %s" % version)
-
-       def check_profile_blob(self, profile):
-               """
-                       This method checks if the blob is sane.
-               """
-               checks = (
-                       self.__check_attributes,
-                       self.__check_valid_ids,
-                       self.__check_equal_ids,
-                       self.__check_profile_version,
-                       # These checks require at least one database query and should be done
-                       # at last.
-                       self.__check_matching_ids,
-               )
-
-               for check in checks:
-                       check(profile)
-
-               # If we got here, everything is okay and we can go on...
-
        def get_profile_blob(self):
                profile = self.get_argument("profile", None)
 
@@ -118,22 +24,14 @@ class ProfileSendHandler(BaseHandler):
                if not profile:
                        raise tornado.web.HTTPError(400, "No profile received")
 
-               # Try to decode the profile.
+               # Try to decode the profile
                try:
                        return json.loads(profile)
                except json.decoder.JSONDecodeError as e:
                        raise tornado.web.HTTPError(400, "Profile could not be decoded: %s" % e)
 
-       # The GET method is only allowed in debugging mode.
-       def get(self, public_id):
-               if not self.application.settings["debug"]:
-                       raise tornado.web.HTTPError(405)
-
-               return self.post(public_id)
-
        def post(self, public_id):
                profile_blob = self.get_profile_blob()
-               #self.check_profile_blob(profile_blob)
 
                # Handle the profile.
                with self.db.transaction():
@@ -141,8 +39,8 @@ class ProfileSendHandler(BaseHandler):
                                self.fireinfo.handle_profile(public_id, profile_blob,
                                        country_code=self.current_country_code)
 
-                       except fireinfo.ProfileParserError as e:
-                               raise tornado.web.HTTPError(400, "Could not parse profile: %s" % e)
+                       except ValueError as e:
+                               raise tornado.web.HTTPError(400, "Could not process profile: %s" % e)
 
                self.finish("Your profile was successfully saved to the database.")
 
@@ -150,13 +48,17 @@ class ProfileSendHandler(BaseHandler):
 class IndexHandler(BaseHandler):
        def get(self):
                data = {
+                       "when"           : self.when,
+
                        # Release
                        "latest_release" : self.backend.releases.get_latest(),
 
                        # Hardware
                        "arches"         : self.fireinfo.get_arch_map(when=self.when),
                        "cpu_vendors"    : self.fireinfo.get_cpu_vendors_map(when=self.when),
-            "memory_avg"     : self.backend.fireinfo.get_average_memory_amount(when=self.when),
+
+                       # Memory
+                       "memory_avg"     : self.backend.fireinfo.get_average_memory_amount(when=self.when),
 
                        # Virtualization
                        "hypervisors"    : self.fireinfo.get_hypervisor_map(when=self.when),
@@ -171,8 +73,9 @@ class IndexHandler(BaseHandler):
 
 class DriverDetail(BaseHandler):
        def get(self, driver):
-               self.render("fireinfo/driver.html", driver=driver,
-                       driver_map=self.fireinfo.get_driver_map(driver, when=self.when))
+               devices = self.fireinfo.get_devices_by_driver(driver, when=self.when)
+
+               self.render("fireinfo/driver.html", driver=driver, devices=devices)
 
 
 class ProfileHandler(BaseHandler):
@@ -187,11 +90,11 @@ class ProfileHandler(BaseHandler):
 
 class RandomProfileHandler(BaseHandler):
        def get(self):
-               profile_id = self.fireinfo.get_random_profile(when=self.when)
-               if profile_id is None:
-                       raise tornado.web.HTTPError(404)
+               profile = self.fireinfo.get_random_profile(when=self.when)
+               if not profile:
+                       raise tornado.web.HTTPError(404, "Could not find a random profile")
 
-               self.redirect("/profile/%s" % profile_id)
+               self.redirect("/profile/%s" % profile.profile_id)
 
 
 class ReleasesHandler(BaseHandler):
@@ -206,13 +109,7 @@ class ReleasesHandler(BaseHandler):
 
 class ProcessorsHandler(BaseHandler):
        def get(self):
-               flags = {}
-
-               for platform in ("arm", "x86"):
-                       flags[platform] = \
-                               self.fireinfo.get_common_cpu_flags_by_platform(platform, when=self.when)
-
-               return self.render("fireinfo/processors.html", flags=flags)
+               return self.render("fireinfo/processors.html", when=self.when)
 
 
 class VendorsHandler(BaseHandler):
@@ -273,4 +170,7 @@ class AdminIndexHandler(BaseHandler):
        def get(self):
                with_data, total = self.backend.fireinfo.get_active_profiles()
 
-               self.render("fireinfo/admin.html", with_data=with_data, total=total)
+               histogram = self.backend.fireinfo.get_profile_histogram()
+
+               self.render("fireinfo/admin.html", with_data=with_data, total=total,
+                       histogram=histogram)
index c7dc319090f496452b14b0a7f1fdc3102291b329..faa6d40661b7eef165278f0a0f260b128a141b02 100644 (file)
@@ -19,7 +19,7 @@ class ImageHandler(base.BaseHandler):
        def get(self, profile_id, image_id):
                when = self.get_argument_date("when", None)
 
-               profile = self.fireinfo.get_profile_with_data(profile_id, when=when)
+               profile = self.fireinfo.get_profile(profile_id, when=when)
                if not profile:
                        raise tornado.web.HTTPError(404, "Profile '%s' was not found." % profile_id)
 
index 43cfd9a6b1a3df13bf3bb3e18239a061c31aad8a..574f9ac918a036ea840477f42450ecaf6447207e 100644 (file)
@@ -10,6 +10,11 @@ class UIModule(tornado.web.UIModule):
                return self.handler.backend
 
 
+class IPFireLogoModule(UIModule):
+       def render(self, suffix=None):
+               return self.render_string("modules/ipfire-logo.html", suffix=suffix)
+
+
 class ChristmasBannerModule(UIModule):
        def render(self):
                return self.render_string("modules/christmas-banner.html")
index 53b3434122901cf48418afc424558ccbf6f6c2c9..246a930070a711672e004755e0a540a5d8047b62 100644 (file)
@@ -45,7 +45,7 @@ class ShowHandler(base.BaseHandler):
 
 
 class AvatarHandler(base.BaseHandler):
-       def get(self, uid):
+       async def get(self, uid):
                # Get the desired size of the avatar file
                size = self.get_argument("size", None)
 
@@ -65,14 +65,14 @@ class AvatarHandler(base.BaseHandler):
                self.set_expires(31536000)
 
                # Resize avatar
-               avatar = account.get_avatar(size)
+               avatar = await account.get_avatar(size)
 
                # If there is no avatar, we serve a default image
                if not avatar:
                        logging.debug("No avatar uploaded for %s" % account)
 
                        # Generate a random avatar with only one letter
-                       avatar = self._get_avatar(account, size=size)
+                       avatar = await self._get_avatar(account, size=size)
 
                # Guess content type
                type = imghdr.what(None, avatar)
@@ -91,7 +91,7 @@ class AvatarHandler(base.BaseHandler):
                # Deliver payload
                self.finish(avatar)
 
-       def _get_avatar(self, account, size=None, **args):
+       async def _get_avatar(self, account, size=None, **args):
                letter = ("%s" % account)[0].upper()
 
                if size is None:
@@ -105,12 +105,12 @@ class AvatarHandler(base.BaseHandler):
                key = "avatar:letter:%s:%s" % (letter, size)
 
                # Fetch avatar from the cache
-               avatar = self.memcached.get(key)
+               avatar = await self.backend.cache.get(key)
                if not avatar:
                        avatar = self._make_avatar(letter, size=size, **args)
 
                        # Cache for forever
-                       self.memcached.set(key, avatar)
+                       await self.backend.cache.set(key, avatar)
 
                return avatar
 
@@ -162,7 +162,7 @@ class EditHandler(base.BaseHandler):
                self.render("users/edit.html", account=account, countries=countries.get_all())
 
        @tornado.web.authenticated
-       def post(self, uid):
+       async def post(self, uid):
                account = self.backend.accounts.get_by_uid(uid)
                if not account:
                        raise tornado.web.HTTPError(404, "Could not find account %s" % uid)