diff --git a/coffee_scale/mqtt.py b/coffee_scale/mqtt.py index 76751bc..3a984fb 100644 --- a/coffee_scale/mqtt.py +++ b/coffee_scale/mqtt.py @@ -48,10 +48,10 @@ def on_message(client, userdata, msg): def on_disconnect(client, userdata, rc): if rc != 0: - print("Unexpected disconnection.") + logging.warning("MQTT unexpectedly disconnected.") else: client.loop_stop(force=False) - print("Disconnected") + logging.warning("MQTT disconnected.") def get_latest(): 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..8cc29e1 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..6805637 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..0636f3d 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,9 +182,8 @@ 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") if not form.is_valid(): return HttpResponseBadRequest('{"status": "failure",' '"error": "invalid data supplied"}') @@ -181,6 +198,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 +209,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 +219,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 +227,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 +244,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 +252,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) diff --git a/members/admin.py b/members/admin.py index b150c1d..6607a19 100644 --- a/members/admin.py +++ b/members/admin.py @@ -1,3 +1,5 @@ +"""Admin site registers for Members app.""" + from django.contrib import admin from members.models import Member, Request, Payment, MemberConflict diff --git a/members/apps.py b/members/apps.py index f773ade..77801d7 100644 --- a/members/apps.py +++ b/members/apps.py @@ -1,5 +1,9 @@ +"""App configurations for members app.""" + from django.apps import AppConfig class MembersConfig(AppConfig): + """Class for Members app configurations.""" + name = 'members' diff --git a/members/forms.py b/members/forms.py index a6e47ce..5622123 100644 --- a/members/forms.py +++ b/members/forms.py @@ -1,19 +1,96 @@ +"""File containing member forms.""" + from django import forms from django.utils.translation import ugettext_lazy as _ from members.models import Member, Payment, Request +import csv +import datetime +import logging + + +class CSVValidationError(Exception): + def __init__(self, form_errors): + self.form_errors = form_errors + class MemberForm(forms.ModelForm): + """Member model form.""" class Meta: + """Meta for Member model form.""" + model = Member - fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR'] + fields = ['first_name', 'last_name', 'email', 'POR', 'AYY', 'jas'] + + class ImportResult: + def __init__(self, members, payments): + self.members = members + self.payments = payments + + def clean_email(self): + email = self.cleaned_data['email'] + + if Member.objects.filter(email=email).exists(): + raise forms.ValidationError('Member with email "{}" already exists.'.format(email), code='exists') + + return email + + def clean_jas(self): + return bool(int(self.data['jas'])) + + def clean_AYY(self): + return bool(int(self.data['AYY'])) + + @staticmethod + def csv_to_models(data, payment_source='AYY'): + clean_data = data.strip().split('\n') + clean_data = [row.rstrip(',') for row in clean_data] + csv_reader = csv.DictReader(clean_data, fieldnames=MemberForm.Meta.fields) + + members = [] + payments = [] + for line in csv_reader: + for key, value in line.items(): + line[key] = value.strip() + + email = line['email'] + member_exists = False + if Member.objects.filter(email=email).exists(): + member_exists = True + + if not member_exists: + form = MemberForm(line) + if not form.is_valid(): + raise CSVValidationError(form.errors) + + model = form.save(commit=False) + members.append(model) + + else: + member = Member.objects.get(email=email) + payment_data = { + 'source': payment_source, + 'member': member.id, + 'date': datetime.datetime.now(), + } + form = PaymentForm(payment_data) + if not form.is_valid(): + raise CSVValidationError(form.errors) + + model = form.save(commit=False) + payments.append(model) + + return MemberForm.ImportResult(members, payments) class PaymentForm(forms.ModelForm): + """Payment model form.""" class Meta: + """Meta for Payment model form.""" + model = Payment fields = ['date', 'source', 'member'] labels = { @@ -22,7 +99,10 @@ class PaymentForm(forms.ModelForm): class ApplicationForm(forms.ModelForm): + """Member application model form.""" class Meta: + """Meta for application model form.""" + model = Request fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR'] diff --git a/members/migrations/0014_auto_20170920_1457.py b/members/migrations/0014_auto_20170920_1457.py new file mode 100644 index 0000000..1397954 --- /dev/null +++ b/members/migrations/0014_auto_20170920_1457.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 11:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('members', '0013_auto_20170601_1822'), + ] + + operations = [ + migrations.AlterField( + model_name='member', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='Email'), + ), + migrations.AlterField( + model_name='request', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='Email'), + ), + ] diff --git a/members/models.py b/members/models.py index a8c46d0..cbcb9d4 100644 --- a/members/models.py +++ b/members/models.py @@ -1,32 +1,35 @@ +"""File containing Members app models.""" + from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from datetime import datetime import csv -import logging class BaseMember(models.Model): - ''' - Base model for member. - ''' + """Abstract base model for member.""" + first_name = models.CharField(_("First name"), max_length=127) last_name = models.CharField(_("Last name"), max_length=127) - email = models.EmailField(_("Email")) - POR = models.CharField(_("Place of residence"), max_length=255) # place of residence + email = models.EmailField(_("Email"), unique=True) + POR = models.CharField(_("Place of residence"), + max_length=255) # place of residence AYY = models.BooleanField(_("AYY"), default=False) jas = models.BooleanField(_("JAS"), default=False) class Meta: + """Meta for base member model.""" + abstract = True def __str__(self): + """Return member last name, first name and email.""" return "{} {}, {}".format(self.last_name, self.first_name, self.email) @staticmethod def from_csv(data): - print("Imported CSV data: {}".format(data)) clean_data = data.strip().split('\n') csv_reader = csv.reader(clean_data) @@ -50,6 +53,7 @@ class BaseMember(models.Model): return True def as_array(self): + """Return member model as an array.""" return [ self.first_name, self.last_name, @@ -61,21 +65,20 @@ class BaseMember(models.Model): class Request(BaseMember): - ''' - Member request model represents one member request. - ''' + """Member request model represents one member request.""" + submitted = models.DateTimeField(_('Submitted'), default=timezone.now) def to_member(self): + """Convert array to member model.""" member = Member.from_array(self.as_array()) return member class Payment(models.Model): - ''' - Payment model representing one payment event - ''' + """Payment model representing one payment event.""" + date = models.DateTimeField(_('Date'), default=datetime.now) source = models.CharField(_('Source'), choices=[ ('AYY', _('AYY')), @@ -90,16 +93,17 @@ class Payment(models.Model): related_name='payments') def __str__(self): + """Return payment id and date.""" return 'Payment no. {}, {}'.format(self.id, str(self.date)) class Member(BaseMember): - ''' - Member model represets one member on the registry. - ''' + """Member model represets one member on the registry.""" + created = models.DateTimeField(_('Created'), default=datetime.now) def last_paid(self): + """Return member's last payment.""" try: payments = Payment.objects.filter(member=self) latest = payments.latest('date') @@ -110,6 +114,7 @@ class Member(BaseMember): @staticmethod def from_array(array): + """Create member from array.""" if len(array) != 6: raise Exception("Invalid array length for member instantiation") @@ -124,16 +129,21 @@ class Member(BaseMember): class MemberConflict(models.Model): + """Model representing member conflict situation.""" - first_member = models.ForeignKey('Member', related_name='%(class)s_first_member') - second_member = models.ForeignKey('Member', related_name='%(class)s_second_member') + first_member = models.ForeignKey( + 'Member', related_name='%(class)s_first_member') + second_member = models.ForeignKey( + 'Member', related_name='%(class)s_second_member') @property def first_member_form(self): + """Get first member form.""" return MemberForm(instance=self.first_member) @property def second_member_form(self): + """Get second member form.""" return MemberForm(instance=self.second_member) diff --git a/members/serializers.py b/members/serializers.py index 8273291..9d1ff21 100644 --- a/members/serializers.py +++ b/members/serializers.py @@ -1,10 +1,17 @@ +"""File containing member serializers.""" + from rest_framework import serializers from members.models import Member class MemberSerializer(serializers.ModelSerializer): + """Model serializer for member.""" + paid = serializers.DateTimeField(source='last_paid') class Meta: + """Meta of member serializer.""" + model = Member - fields = ('id', 'first_name', 'last_name', 'email', 'POR', 'AYY', 'jas', 'created', 'paid') + fields = ('id', 'first_name', 'last_name', 'email', + 'POR', 'AYY', 'jas', 'created', 'paid') diff --git a/members/tables.py b/members/tables.py index 1910c2d..a6274ec 100644 --- a/members/tables.py +++ b/members/tables.py @@ -1,3 +1,5 @@ +"""File containing member application django tables.""" + import django_tables2 as tables from django.utils.translation import ugettext as _ @@ -5,43 +7,56 @@ from members.models import Member, Payment, Request class MemberTable(tables.Table): + """Table for member.""" - last_paid = tables.DateTimeColumn(accessor='last_paid', verbose_name=_('Last paid')) + last_paid = tables.DateTimeColumn( + accessor='last_paid', verbose_name=_('Last paid')) options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for member table.""" + model = Member class PaymentTable(tables.Table): + """Table for payments.""" member = tables.Column(accessor='member', verbose_name=_('Member')) options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for payment table.""" + model = Payment class RequestTable(tables.Table): + """Table for member applications.""" options = tables.TemplateColumn( - '' + + ('') + _('Edit') + '', verbose_name=_('Options') ) class Meta: + """Meta for request table.""" + model = Request diff --git a/members/templates/error.html b/members/templates/error.html index b8c30ae..745eb56 100644 --- a/members/templates/error.html +++ b/members/templates/error.html @@ -9,7 +9,7 @@