diff --git a/infoscreen/admin.py b/infoscreen/admin.py index b4f9217..d7b314c 100644 --- a/infoscreen/admin.py +++ b/infoscreen/admin.py @@ -1,3 +1,5 @@ +"""Admin site registers.""" + from django.contrib import admin from infoscreen.models import Rotation, InfoItem, InfoInstance from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem diff --git a/infoscreen/apps.py b/infoscreen/apps.py index 0789a89..8b14bab 100644 --- a/infoscreen/apps.py +++ b/infoscreen/apps.py @@ -1,5 +1,9 @@ +"""Django apps configuration file.""" + from django.apps import AppConfig class InfoscreenConfig(AppConfig): + """Infoscreen app configuration.""" + name = 'infoscreen' diff --git a/infoscreen/hsl_fetcher.py b/infoscreen/hsl_fetcher.py index 89c6aec..9828391 100644 --- a/infoscreen/hsl_fetcher.py +++ b/infoscreen/hsl_fetcher.py @@ -1,3 +1,5 @@ +"""File containing Infoscreen HSL data fetcher classes.""" + import urllib.request import json import logging @@ -9,36 +11,49 @@ from infoscreen.models import HSLDataModel class HSLFetcher: + """Main class of Infoscreen HSL fetcher.""" last_fetched = datetime.fromtimestamp(0) # epoch INTERVAL = 1 # minutes - logging.info("Set up scheduled HSL API fetch every {} minutes".format(INTERVAL)) + logging.info( + "Set up scheduled HSL API fetch every {} minutes".format(INTERVAL)) def fetch_if_needed(self): - if datetime.now() - HSLFetcher.last_fetched > timedelta(minutes=HSLFetcher.INTERVAL): + """Check if new fetch from HSL API is needed.""" + if (datetime.now() - HSLFetcher.last_fetched > + timedelta(minutes=HSLFetcher.INTERVAL)): self.fetch() def fetch(self): + """Fetch data from HSL API.""" location_coords = (2545565, 6675319) src = urllib.request.urlopen( - "https://api.reittiopas.fi/hsl/prod/?userhash={}&request=stops_area¢er_coordinate={},{}" - .format(settings.HSL_USERHASH, location_coords[0], location_coords[1]))\ + ("https://api.reittiopas.fi/hsl/prod/?userhash={}" + "&request=stops_area¢er_coordinate={},{}") + .format(settings.HSL_USERHASH, location_coords[0], + location_coords[1]))\ .read().decode("utf-8") data = json.loads(src) arr = [] - time = datetime.now() + timedelta(minutes=settings.HSL_DEPARTURE_THRESHOLD) + time = (datetime.now() + + timedelta(minutes=settings.HSL_DEPARTURE_THRESHOLD)) time = "{0:02d}{0:02d}".format(time.hour, time.minute) for element in data: src = urllib.request.urlopen( - "https://api.reittiopas.fi/hsl/prod/?userhash={}&request=stop&code={}&dep_limit=20&time={}" - .format(settings.HSL_USERHASH, element['code'], time)).read().decode("utf-8") + ("https://api.reittiopas.fi/hsl/prod/?userhash={}" + "&request=stop&code={}&dep_limit=20&time={}") + .format(settings.HSL_USERHASH, element['code'], time) + ).read().decode("utf-8") parsed = json.loads(src)[0] - arr.append({"name": parsed['name_fi'], "lines": parsed['lines'], - "dist": element['dist'], "departures": parsed['departures']}) + arr.append({ + "name": parsed['name_fi'], + "lines": parsed['lines'], + "dist": element['dist'], + "departures": parsed['departures']}) model_arr = HSLDataModel.objects.all() count = len(model_arr) @@ -53,4 +68,5 @@ class HSLFetcher: now = datetime.now() HSLFetcher.last_fetched = now - logging.info("Fetched HSL timetable data with size {} bytes.".format(len(src))) + logging.info( + "Fetched HSL timetable data with size {} bytes.".format(len(src))) diff --git a/infoscreen/models.py b/infoscreen/models.py index 5f47cc5..04421da 100644 --- a/infoscreen/models.py +++ b/infoscreen/models.py @@ -1,3 +1,5 @@ +"""File containing Infoscreen models.""" + from datetime import datetime from django.db import models @@ -9,31 +11,40 @@ from django.utils.translation import ugettext as _ class InfoItem(models.Model): + """Abstract model representing single Infoscreen item.""" class __meta__: abstract = True name = models.CharField(max_length=255) - expire_date = models.DateTimeField(blank=True, null=True) # None means never expiring item + # expire_date = None means never expiring item + expire_date = models.DateTimeField(blank=True, null=True) display_name = "Default item" def get_template_url(self): - raise NotImplementedError("inheriting classes must implement get_template_url") + """Get infoscreen template url.""" + raise NotImplementedError( + "inheriting classes must implement get_template_url") @staticmethod def get_create_template_url(): - raise NotImplementedError("inheriting classes must implement get_create_template_url") + """Get create infoscreen template url command.""" + raise NotImplementedError( + "inheriting classes must implement get_create_template_url") @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -48,6 +59,7 @@ class InfoItem(models.Model): self.save() def get_dict(self): + """Convert django model to dict and return it.""" return { 'id': self.id, 'name': self.name, @@ -59,65 +71,86 @@ class InfoItem(models.Model): } def delete(self): - # since generic foreign keys suck, delete info items pointing here manually - InfoInstance.objects.filter(item_id=self.id, item_type=ContentType.objects.get_for_model(self)).delete() + """Delete infoinstance object.""" + # since generic foreign keys suck, delete info + # items pointing here manually + InfoInstance.objects.filter( + item_id=self.id, + item_type=ContentType.objects.get_for_model(self)).delete() super().delete() @classmethod def get_subclasses(cls): + """Get item subclasses.""" for subclass in cls.__subclasses__(): yield from subclass.get_subclasses() yield subclass def __str__(self): + """Return class name.""" return self.name class ABBInfoItem(InfoItem): + """Class for ABB Infoscreen item.""" + display_name = _("ABB jobs") def get_template_url(self): + """Return ABB infoitem template url.""" return "/static/html/abb.html" @staticmethod def get_create_template_url(): + """Call create ABB infoitem template url command.""" return "/static/html/abb_create.html" class ApyInfoItem(InfoItem): + """Class for APY Infoscreen item.""" + display_name = _("APY Item") def get_template_url(self): + """Return APY infoitem template url.""" return "/static/html/apy.html" @staticmethod def get_create_template_url(): + """Call create APY infoitem template url command.""" return "/static/html/apy_create.html" class ExternalWebsiteInfoItem(InfoItem): + """Class for external website info item.""" + display_name = _("External website") url = models.URLField() def get_template_url(self): + """Return external website infoitem template url.""" return "/static/html/external_website.html?url={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create external website infoitem template url command.""" return "/static/html/external_website_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'url': self.url} return d @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def get_list(self): + """Return list containing infoitem data.""" return { 'id': self.id, 'name': self.name, @@ -125,9 +158,11 @@ class ExternalWebsiteInfoItem(InfoItem): } def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -144,99 +179,130 @@ class ExternalWebsiteInfoItem(InfoItem): class SossoInfoItem(InfoItem): + """Class for Sosso Infoscreen item.""" + display_name = _("Sössö articles") def get_template_url(self): + """Return Sosso infoitem template url.""" return "/static/html/sosso.html" @staticmethod def get_create_template_url(): + """Call create Sosso infoitem template url command.""" return "/static/html/sosso_create.html" class EventInfoItem(InfoItem): + """Class for Event Infoscreen item.""" + display_name = _("Events") def get_template_url(self): + """Return Event infoitem template url.""" return "/static/html/events.html" @staticmethod def get_create_template_url(): + """Call create Event infoitem template url command.""" return "/static/html/events_create.html" class ImageInfoItem(InfoItem): + """Class for Image Infoscreen item.""" + display_name = _("Image") img = models.ImageField(upload_to="infoimages/") def get_template_url(self): - # get param to avoid angular from optimizing same template with different options + """Return Image infoitem template url.""" + # get param to avoid angular from optimizing same template + # with different options return "/static/html/generic_image.html?img={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create Image infoitem template url command.""" return "/static/html/generic_image_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'img': self.img.url} return d class VideoInfoItem(InfoItem): + """Class for Video Infoscreen item.""" + display_name = ("Video") video = models.FileField(upload_to="infovideos/") def get_template_url(self): + """Return Video infoitem template url.""" return "/static/html/generic_video.html?video={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create Video infoitem template url command.""" return "/static/html/generic_video_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'video': self.video.url} return d class HslInfoItem(InfoItem): + """Class for HSL Infoscreen item.""" + display_name = _("HSL timetables") def get_template_url(self): + """Return HSL infoitem template url.""" return "/static/html/hsl.html" @staticmethod def get_create_template_url(): + """Call create HSL infoitem template url command.""" return "/static/html/hsl_create.html" class ExternalImageInfoItem(InfoItem): + """Class for External Image Infoscreen item.""" + display_name = _("External image") url = models.URLField() def get_template_url(self): + """Return External Image infoitem template url.""" return "/static/html/generic_image.html?img={}".format(self.name) @staticmethod def get_create_template_url(): + """Call create External Image infoitem template url command.""" return "/static/html/generic_external_image_create.html" def get_dict(self): + """Convert django model to dict and return it.""" d = super().get_dict() d["options"] = {'img': self.url} return d @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" item = cls() item.update_from_dict(d) return item def update_from_dict(self, d): + """Update model based on given dict.""" try: expire_date = d.pop('expire_date', None) - self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S") + self.expire_date = datetime.strptime( + expire_date, "%Y-%m-%d %H:%M:%S") except: pass @@ -253,6 +319,8 @@ class ExternalImageInfoItem(InfoItem): class InfoInstance(models.Model): + """Class for Info instance in Infoscreen.""" + rotation = models.ForeignKey('Rotation', related_name='instances') duration = models.FloatField(default=15.0) # seconds # generic relation to some kind of InfoItem @@ -262,6 +330,7 @@ class InfoInstance(models.Model): @classmethod def create_from_dict(cls, d): + """Convert given dict to model.""" try: rotation = Rotation.objects.get(pk=int(d["rotation_id"])) ct = ContentType.objects.get_for_id(int(d["item_type"])) @@ -279,6 +348,7 @@ class InfoInstance(models.Model): raise RuntimeError("error while adding instance to db") def get_dict(self): + """Convert django model to dict and return it.""" return { 'id': self.id, 'item': self.item.get_dict(), @@ -286,17 +356,24 @@ class InfoInstance(models.Model): } def __str__(self): - return "{}: {} ({}s)".format(self.rotation.name, self.item.name, self.duration) + """Return model name.""" + return "{}: {} ({}s)".format( + self.rotation.name, self.item.name, self.duration) class Rotation(models.Model): + """Class for rotation model.""" + name = models.CharField(max_length=255) def get_dict(self): - # exclude expired items from rotation (note: using tricky syntax to avoid excluding items with no expire_date) + """Convert django model to dict and return it.""" + # exclude expired items from rotation (note: using tricky syntax + # to avoid excluding items with no expire_date) now = timezone.now() instances = self.instances.all() - filtered = filter(lambda i: (i.item.expire_date or now) >= now, list(instances)) + filtered = filter(lambda i: (i.item.expire_date or now) + >= now, list(instances)) instance_list = list(map(lambda i: i.get_dict(), filtered)) return { @@ -306,29 +383,32 @@ class Rotation(models.Model): } def get_list(self): + """Return list containing infoitem data.""" return { 'id': self.id, 'name': self.name, } def __str__(self): + """Return model name.""" return self.name class ImageUploadForm(forms.Form): - ''' - Form used to handle imageuploads to - infoscreen app - ''' + """Form used to handle imageuploads to infoscreen app.""" + name = forms.CharField() image = forms.ImageField() class UploadFileForm(forms.Form): + """Form used for uploading file.""" name = forms.CharField() video = forms.FileField() class HSLDataModel(models.Model): + """Model representing HSL data.""" + data = models.TextField(default="", editable=False) diff --git a/infoscreen/tests.py b/infoscreen/tests.py index 561adba..69ee0fa 100644 --- a/infoscreen/tests.py +++ b/infoscreen/tests.py @@ -1,41 +1,37 @@ +"""File containing Infoscreen tests.""" + from django.test import TestCase from infoscreen.models import Rotation from infoscreen.models import SossoInfoItem -from django.http import HttpRequest, HttpResponse +from django.http import HttpRequest import infoscreen.views class InfoscreenTestCase(TestCase): - ''' - Test cases for testing infoscreen methods - ''' + """Test cases for testing infoscreen methods.""" def setUp(self): - ''' - Create some dummy models - ''' + """Create some dummy models.""" Rotation.objects.create(name="test_rot") SossoInfoItem.objects.create() def test_rotation_created(self): - ''' - Check if the dummy model actually exists - ''' + """Check if the dummy model actually exists.""" rot = Rotation.objects.get(name="test_rot") self.assertIsNotNone(rot) def test_sosso_infoitem_created(self): - ''' - Check if the dummy model actually exists - ''' + """Check if the dummy model actually exists.""" item = SossoInfoItem.objects.get() self.assertIsNotNone(item) def test_get_infoitems(self): - ''' - Check if infoItems returns a response with non-zero content length - That would mean that something meaningful has been included in the response - ''' + """ + Check if infoItems returns a response with non-zero content length. + + That would mean that something meaningful has been included + in the response. + """ req = HttpRequest() resp = infoscreen.views.info_items(req) content = resp.content.decode('utf-8') diff --git a/infoscreen/urls.py b/infoscreen/urls.py index 1548bde..5a16fe7 100644 --- a/infoscreen/urls.py +++ b/infoscreen/urls.py @@ -1,3 +1,5 @@ +"""File containing infoscreen urls.""" + from django.conf.urls import url from infoscreen.views import index diff --git a/infoscreen/views.py b/infoscreen/views.py index 055cee6..d2347f2 100644 --- a/infoscreen/views.py +++ b/infoscreen/views.py @@ -1,3 +1,5 @@ +"""File containing infoscreen views.""" + from django.shortcuts import render from django.http import HttpResponse, HttpResponseBadRequest from django.views.decorators.csrf import ensure_csrf_cookie @@ -13,7 +15,8 @@ import threading import requests from infoscreen.models import Rotation, InfoItem, InfoInstance -from infoscreen.models import ABBInfoItem, ExternalImageInfoItem, ImageInfoItem, SossoInfoItem, HslInfoItem +from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem, + ImageInfoItem, SossoInfoItem, HslInfoItem) from infoscreen.models import EventInfoItem from infoscreen.models import ExternalWebsiteInfoItem from infoscreen.models import ImageUploadForm @@ -24,15 +27,18 @@ from infoscreen.hsl_fetcher import HSLFetcher def index(request, idx, *args, **kwargs): + """Render infoscreen index page.""" return render(request, 'infoscreen_index.html', {'rotation': idx}) @permission_required('infoscreen.change_infoinstance', login_url='/login') def admin(request, *args, **kwargs): + """Render infoscreen admin page.""" return render(request, 'infoscreen_admin.html', {}) def default(request, *args, **kwargs): + """Try getting first rotation item.""" try: first = Rotation.objects.all()[0].id except: @@ -41,11 +47,14 @@ def default(request, *args, **kwargs): def get_apy_json(request): - return HttpResponse(requests.get("https://api-diilikone.apy.fi/deals/top-groups").text) + """Render APY diilikone page.""" + return HttpResponse( + requests.get("https://api-diilikone.apy.fi/deals/top-groups").text) @require_http_methods(["GET"]) def rotation(request, idx, *args, **kwargs): + """Get rotation.""" try: rotation = Rotation.objects.get(pk=idx) except Rotation.DoesNotExist: @@ -57,6 +66,7 @@ def rotation(request, idx, *args, **kwargs): def create_item_generator(model): + """Create Infoscreen item generator.""" @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('infoscreen.change_infoinstance', login_url='/login') @@ -64,16 +74,19 @@ def create_item_generator(model): try: data = json.loads(request.body.decode("utf-8")) except ValueError: - return HttpResponseBadRequest('{"status":"failure","error":"invalid json supplied"}') + return HttpResponseBadRequest( + '{"status":"failure","error":"invalid json supplied"}') try: model.create_from_dict(data) return HttpResponse('{"status":"success"}') except RuntimeError as e: - return HttpResponseBadRequest(json.dumps({"status": "failure", "error": str(e)})) + return HttpResponseBadRequest( + json.dumps({"status": "failure", "error": str(e)})) return create_item def delete_item_generator(model): + """Delete Infoscreen item generator.""" @ensure_csrf_cookie @require_http_methods(["DELETE"]) @permission_required('infoscreen.change_infoinstance', login_url='/login') @@ -100,6 +113,7 @@ def delete_item_generator(model): @permission_required('infoscreen.change_infoinstance', login_url='/login') @require_http_methods(["DELETE"]) def delete_info_item(request, *args, **kwargs): + """Delete info item.""" type_id = kwargs.pop("type_id", 0) idx = kwargs.pop("idx", 0) if True: @@ -120,12 +134,14 @@ def delete_info_item(request, *args, **kwargs): @require_http_methods(["GET"]) def rotations(request, *args, **kwargs): + """Return rotation lists.""" rotations = list(map(lambda r: r.get_list(), Rotation.objects.all())) return HttpResponse(json.dumps(rotations)) @require_http_methods(["GET"]) def info_types(request, *args, **kwargs): + """Return info item types.""" types = [] classes = InfoItem.get_subclasses() for c in classes: @@ -137,6 +153,7 @@ def info_types(request, *args, **kwargs): def info_items(request, *args, **kwargs): + """Return Infoscreen items.""" items = [] classes = InfoItem.get_subclasses() for c in classes: @@ -149,6 +166,7 @@ def info_items(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.change_infoinstance', login_url='/login') def create_image_item(request, *args, **kwargs): + """Create image Infoscreen item.""" form = ImageUploadForm(request.POST, request.FILES) if not form.is_valid(): return HttpResponseBadRequest('{"status": "failure",' @@ -164,6 +182,7 @@ def create_image_item(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.change_infoinstance', login_url='/login') def create_video_item(request, *args, **kwargs): + """Create video Infoscreen item.""" form = UploadFileForm(request.POST, request.FILES) print(form.errors) print("hurdurr") @@ -181,6 +200,7 @@ def create_video_item(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.add_rotation', login_url='/login') def create_rotation(request, *args, **kwargs): + """Create rotation.""" try: data = json.loads(request.body.decode("utf-8")) except: @@ -191,7 +211,8 @@ def create_rotation(request, *args, **kwargs): Rotation.objects.create(name=name) resp = HttpResponse(status=200) except: - resp = HttpResponse('{"error" : "could not create rotation!"}', status=400) + resp = HttpResponse( + '{"error" : "could not create rotation!"}', status=400) return resp @@ -200,7 +221,7 @@ def create_rotation(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('infoscreen.delete_rotation', login_url='/login') def delete_rotation(request, *args, **kwargs): - + """Delete rotation.""" id = kwargs.pop("id", 0) logging.warning("Deleting rotation with id={}".format(id)) @@ -208,13 +229,15 @@ def delete_rotation(request, *args, **kwargs): Rotation.objects.filter(id=id).delete() resp = HttpResponse(status=200) except: - resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400) + resp = HttpResponse( + '{"error" : "could not delete rotation!"}', status=400) return resp @require_http_methods(["GET"]) def hsl_timetable_settings(request, *args, **kwargs): + """Set HSL timetable settings.""" d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD, "hurry_threshold": settings.HSL_HURRY_THRESHOLD} resp = json.dumps(d) @@ -223,7 +246,7 @@ def hsl_timetable_settings(request, *args, **kwargs): @require_http_methods(["GET"]) def CurrentHSLView(request, *args, **kwargs): - + """Get HSL data and return it.""" fetcher = HSLFetcher() fetcherThread = threading.Thread(target=fetcher.fetch_if_needed, args=[]) fetcherThread.setDaemon(False) @@ -231,7 +254,9 @@ def CurrentHSLView(request, *args, **kwargs): data = HSLDataModel.objects.all() if len(data) < 1: - return HttpResponse('{"error" : "Could not find timetables from database."}', status=500) + return HttpResponse( + '{"error" : "Could not find timetables from database."}', + status=500) return HttpResponse(data[len(data) - 1].data, status=200)