diff --git a/sdbs_infra/dashboard/admin.py b/sdbs_infra/dashboard/admin.py index 50e8888..ad6c26d 100644 --- a/sdbs_infra/dashboard/admin.py +++ b/sdbs_infra/dashboard/admin.py @@ -1,11 +1,16 @@ from django.contrib import admin from ordered_model.admin import OrderedModelAdmin -from sdbs_infra.dashboard.models import Service +from sdbs_infra.dashboard.models import Service, Link + + +class LinkAdmin(OrderedModelAdmin): + list_display = ('short_name', 'url', 'move_up_down_links') class ServiceAdmin(OrderedModelAdmin): list_display = ('short_name', 'url', 'move_up_down_links') +admin.site.register(Link, LinkAdmin) admin.site.register(Service, ServiceAdmin) diff --git a/sdbs_infra/dashboard/migrations/0007_link.py b/sdbs_infra/dashboard/migrations/0007_link.py new file mode 100644 index 0000000..8067a07 --- /dev/null +++ b/sdbs_infra/dashboard/migrations/0007_link.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2020-06-20 17:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0006_service_port'), + ] + + operations = [ + migrations.CreateModel( + name='Link', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')), + ('short_name', models.CharField(max_length=64)), + ('image', models.ImageField(blank=True, null=True, upload_to='links')), + ('description', models.TextField(blank=True, null=True)), + ('url', models.URLField()), + ], + options={ + 'verbose_name': 'Important Link', + 'verbose_name_plural': 'Important Links', + }, + ), + ] diff --git a/sdbs_infra/dashboard/models.py b/sdbs_infra/dashboard/models.py index b505434..38be2ef 100644 --- a/sdbs_infra/dashboard/models.py +++ b/sdbs_infra/dashboard/models.py @@ -1,7 +1,5 @@ -import socket from enum import Enum -from bs4 import BeautifulSoup from django.db import models from ordered_model.models import OrderedModel @@ -20,4 +18,18 @@ class Service(OrderedModel): url = models.URLField() def __str__(self): - return f"{self.short_name} ({self.url})" \ No newline at end of file + return f"{self.short_name} ({self.url})" + + +class Link(OrderedModel): + short_name = models.CharField(null=False, max_length=64) + image = models.ImageField(null=True, blank=True, upload_to='links') + description = models.TextField(null=True, blank=True) + url = models.URLField() + + def __str__(self): + return f"{self.short_name} ({self.url})" + + class Meta: + verbose_name = "Important Link" + verbose_name_plural = "Important Links" diff --git a/sdbs_infra/dashboard/static/main.css b/sdbs_infra/dashboard/static/main.css index ff62529..295b8f3 100644 --- a/sdbs_infra/dashboard/static/main.css +++ b/sdbs_infra/dashboard/static/main.css @@ -25,10 +25,17 @@ a { h1, h2 { text-align: center; - margin: 1rem 0; } -main { +h1 { + margin: 1rem 0 0 0; +} + +h2 { + margin: 2rem 0 0 0; +} + +.boxes { width: 100%; display: flex; flex-wrap: wrap; @@ -36,7 +43,7 @@ main { margin-bottom: -1rem; } -.service { +.box { flex-basis: 20%; margin: 1rem; display: flex; @@ -45,44 +52,63 @@ main { @media screen and (max-width: 1000px) { - .service { + .box { flex-basis: 40%; } } @media screen and (max-width: 600px) { - .service { + .box { flex-basis: 100%; } } -.service-content { +.box-content { flex-grow: 1; border: 1px solid white; padding: 2rem; display: flex; - flex-direction: column; align-items: center; - justify-content: center; } -.service-content img { - flex-grow: 1; - min-width: 50%; - max-width: 100%; +.link .box-content { + flex-direction: row; + justify-content: space-evenly; +} + +.service .box-content { + flex-direction: column; + justify-content: flex-end; +} + +.box-content img { filter: grayscale(100%); image-rendering: crisp-edges; text-align: center; } -.service .label h3 { +.service img { + min-width: 50%; + max-width: 100%; +} + +.link img { + min-height: 1rem; + max-height: 4rem; +} + +.box .label h3 { margin: 1rem 0; text-align: center; } -.service .label .description { +.link .label h3 { + margin: 0 0 1rem; +} + +.box .label .description { font-size: 10pt; margin: 0; align-self: flex-start; diff --git a/sdbs_infra/dashboard/templates/index.html b/sdbs_infra/dashboard/templates/index.html index 08b5a46..00a3d70 100644 --- a/sdbs_infra/dashboard/templates/index.html +++ b/sdbs_infra/dashboard/templates/index.html @@ -10,12 +10,30 @@

/-\ infrastructure

-

status page (internal services)

-
+

important links

+ +

internal services

+
{% for service in services %} - -
+ +
{% if service.image_url %} image for {{ service.short_name }} {% endif %} @@ -31,7 +49,7 @@
{% endfor %} -
+
VPS STATS — {{ vps_stats|safe }}
diff --git a/sdbs_infra/dashboard/views.py b/sdbs_infra/dashboard/views.py index c996234..a59ca1e 100644 --- a/sdbs_infra/dashboard/views.py +++ b/sdbs_infra/dashboard/views.py @@ -9,20 +9,45 @@ from bs4 import BeautifulSoup from django.views.generic import TemplateView from humanize import naturalsize -from sdbs_infra.dashboard.models import Service, ServiceStatus +from sdbs_infra.dashboard.models import Service, ServiceStatus, Link class IndexView(TemplateView): template_name = "index.html" def get_context_data(self, **kwargs): + return { + 'links': asyncio.run(self.process_links(list(Link.objects.all()))), 'services': asyncio.run(self.process_services(list(Service.objects.all()))), 'vps_stats': self.vps_stats() } - @staticmethod - async def process_services(services): + async def process_links(self, links): + result = [] + + session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1)) + + for link in links: + index_text = None, None + if not link.image: + try: + async with session.get(link.url) as response: + index_status, index_text = response.status, await response.text() + except asyncio.TimeoutError: + pass + + image = link.image.url if link.image else self.extract_favicon(link.url, index_text) + + result.append({ + 'image_url': image, + **vars(link) + }) + + await session.close() + return result + + async def process_services(self, services): result = [] session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=5, sock_connect=1)) @@ -49,20 +74,7 @@ class IndexView(TemplateView): else: status = ServiceStatus.UNKNOWN - image = None - if service.image: - image = service.image.url - elif index_text: - parsed_html = BeautifulSoup(index_text, features="html.parser") - link_tags = parsed_html.find_all('link') - for rel in ['apple-touch-icon', 'shortcut', 'icon']: - for link_tag in link_tags: - if rel in link_tag.attrs['rel']: - link = link_tag.attrs['href'] - if service.url not in link: - image = service.url + (link if link.startswith("/") else f"/{link}") - else: - image = link + image = service.image.url if service.image else self.extract_favicon(service.url, index_text) result.append({ 'status': status.value, @@ -71,9 +83,25 @@ class IndexView(TemplateView): }) await session.close() - return result + @staticmethod + def extract_favicon(url, index_text): + if not index_text: + return None + + parsed_html = BeautifulSoup(index_text, features="html.parser") + link_tags = parsed_html.find_all('link') + for rel in ['apple-touch-icon', 'shortcut', 'icon']: + for link_tag in link_tags: + if rel in link_tag.attrs['rel']: + href = link_tag.attrs['href'] + if url not in href: + image = url + (href if href.startswith("/") else f"/{href}") + else: + image = href + return image + # noinspection PyListCreation @staticmethod def vps_stats():