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 @@
- {{ error }} + {{ error|safe }}
diff --git a/members/templates/member_add_many.html b/members/templates/member_add_many.html index fc083eb..9d9b4e8 100644 --- a/members/templates/member_add_many.html +++ b/members/templates/member_add_many.html @@ -5,7 +5,7 @@ {% block content %}
-

Lisää useampi jäsen

+

{% trans "Add many members" %}

@@ -25,9 +25,18 @@
{% csrf_token %} -
+
+
+
+ + +
diff --git a/members/templates/member_add_many_confirm.html b/members/templates/member_add_many_confirm.html new file mode 100644 index 0000000..2cff70a --- /dev/null +++ b/members/templates/member_add_many_confirm.html @@ -0,0 +1,26 @@ +{% extends "members_base.html" %} + +{% load i18n %} + +{% block content %} +
+
+

{% trans "Confirm adding these entries?" %}

+
+ +
+ + {{ members|safe }} +
+
+ + {{ payments|safe }} +
+
+ {% csrf_token %} +
+ +
+ +
+{% endblock content %} diff --git a/members/templates/member_duplicates.html b/members/templates/member_duplicates.html deleted file mode 100644 index c6f2995..0000000 --- a/members/templates/member_duplicates.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "members_base.html" %} - -{% load i18n %} -{% load bootstrap3 %} - -{% block content %} -
-
-

{% trans "Conflicting member entries" %}

-
- -
-

{% blocktrans %}Found conflicting member entries. Choose how to handle the problematic data.{% endblocktrans %}

- - {% for conflict in conflicts %} -
-
- - {{ conflict.first_member_form }} -
-
-
- - {{ conflict.second_member_form }} -
-
-
-
{% csrf_token %} -

{% blocktrans %}Which one has the correct information for this member?{% endblocktrans %}

- - - - -
-
-
- {% endfor %} -
-
-{% endblock content %} diff --git a/members/tests.py b/members/tests.py index 025c3d9..3987d38 100644 --- a/members/tests.py +++ b/members/tests.py @@ -1,28 +1,34 @@ +"""File containing Member app tests.""" + from django.test import TestCase, Client from django.contrib.auth.models import User -import time - from members.models import Member class MemberRegisterTestCase(TestCase): + """Tests member registration.""" def setUp(self): + """Setup testing environment by creating member and admin.""" memb = Member.objects.create(first_name="Tidus", last_name="Tester") - test_admin = User.objects.create_superuser('test_admin', 'myemail@test.com', 'password123') + test_admin = User.objects.create_superuser( + 'test_admin', 'myemail@test.com', 'password123') self.c = Client() def test_member_created(self): + """Test member creation.""" exists = Member.objects.filter(first_name="Tidus").exists() self.assertTrue(exists) def test_import_csv_single_line(self): + """Test csv import only with single line in csv file.""" data = 'Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0' response = self.c.post('/members/import_csv', {'textarea': data}) self.assertIn(response.status_code, [200, 302]) def test_import_csv_multi_line(self): + """Test csv import with multilined csv.""" data = ('Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0\n' 'Reiska, Remontti, remontti@reiska.fi, Värisilmä, 1, 1') diff --git a/members/throttles.py b/members/throttles.py index 0c1e17b..3004a82 100644 --- a/members/throttles.py +++ b/members/throttles.py @@ -1,9 +1,15 @@ +"""File containing throttle rates for API.""" + from rest_framework.throttling import UserRateThrottle class BurstRateThrottle(UserRateThrottle): + """Class for burst rate throttle.""" + scope = 'burst' class SustainedRateThrottle(UserRateThrottle): + """Class for sustained rate throttle.""" + scope = 'sustained' diff --git a/members/urls.py b/members/urls.py index 9738af6..664e5cd 100644 --- a/members/urls.py +++ b/members/urls.py @@ -1,3 +1,5 @@ +"""File containing Member application URLs.""" + from django.conf.urls import url from django.views.generic.base import RedirectView @@ -16,8 +18,7 @@ from members.views import member_update from members.views import member_delete_confirm from members.views import member_delete from members.views import payment_list -from members.views import member_duplicates -from members.views import resolve_conflict +from members.views import add_many_confirm # rest api from members.views import MemberDetail @@ -32,7 +33,8 @@ from members.views import application_form_success # from members.views import validateEmail, validate_success, validate_fail -favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True) +favicon_view = RedirectView.as_view( + url='static/img/favicon.ico', permanent=True) urlpatterns = [ @@ -75,7 +77,8 @@ urlpatterns = [ url(r'^application/success$', application_form_success), # delete confirmation view for applications - url(r'^delete_application_confirm/(?P\d+)$', application_delete_confirm), + url(r'^delete_application_confirm/(?P\d+)$', + application_delete_confirm), # list all payment events url(r'^payments$', payment_list), @@ -89,6 +92,9 @@ urlpatterns = [ # delete confirmation view url(r'^delete_payment_confirm/(?P\d+)$', payment_delete_confirm), + # post endpoint for confirming multiple entries + url(r'^add_many_confirm$', add_many_confirm), + # settings page url(r'^settings$', settings_page), @@ -104,10 +110,4 @@ urlpatterns = [ # rest api url url(r'^api/members/(?P\d+)$', MemberDetail.as_view()), - # member duplicate resolution view - url(r'^duplicates$', member_duplicates), - - # post target for resolving a conflict - url(r'^resolve_conflict$', resolve_conflict) - ] diff --git a/members/views.py b/members/views.py index da03f3d..7d3f33c 100644 --- a/members/views.py +++ b/members/views.py @@ -1,3 +1,5 @@ +"""File containing Members application views.""" + from django.shortcuts import render from django.contrib.auth.decorators import permission_required from django.views.decorators.http import require_http_methods @@ -6,6 +8,7 @@ from django.http import HttpResponse, HttpResponseRedirect from django.core.mail import send_mail from django.conf import settings from django.utils.translation import ugettext as _ +from django.forms.models import model_to_dict # Email validation from django.db.models.signals import post_save @@ -25,21 +28,25 @@ import requests import logging import html import csv +import pickle from smtplib import SMTPAuthenticationError from members.models import Member, Request, Payment, MemberConflict -from members.forms import MemberForm, PaymentForm, ApplicationForm +from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError from members.tables import MemberTable, PaymentTable, RequestTable +def error_view(request, message): + return render(request, 'error.html', {'error': str(message)}) + + def validate_recaptcha(response): - ''' - Recaptcha is used in member applications + """ + Recaptcha is used in member applications. :param response: :return: Boolean, success or not - ''' - + """ values = { 'secret': settings.GOOGLE_RECAPTCHA_SECRET_KEY, 'response': response, @@ -56,6 +63,7 @@ def validate_recaptcha(response): def send_mail_wrapper(subject, message): + """Call send_mail function.""" send_mail(subject, message, 'no-reply@sahkoinsinoorikilta.fi', @@ -64,7 +72,9 @@ def send_mail_wrapper(subject, message): def convert_table_to_html(table, request): - ''' + """ + Convert table to html. + This is a horrible hack for converting a table object to raw html. Even with extensive research I wasn't able to find a way to add a path prefix "e.g. /members/list" to the query strings "e.g. ?sort=foo", so I @@ -76,7 +86,7 @@ def convert_table_to_html(table, request): :param table: Table object from members.tables :param request: HttpRequest :return: Raw html string - ''' + """ table_as_html = table.as_html(request) path = request.path @@ -88,6 +98,7 @@ def convert_table_to_html(table, request): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_list(request, *args, **kwargs): + """Render members list.""" members = Member.objects.all() table = MemberTable(members, @@ -111,6 +122,7 @@ def member_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_add(request, *args, **kwargs): + """Render add member page.""" form = MemberForm() return render(request, 'member_add.html', {'form': form}) @@ -119,19 +131,23 @@ def member_add(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_delete_confirm(request, *args, **kwargs): + """Render member deletion confirmation page.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render(request, 'error.html', + {'error': _('No member id specified')}) else: member = Member.objects.get(id=i) form = MemberForm(instance=member) - return render(request, 'member_delete_confirm.html', {'member_id': i, 'form': form}) + return render(request, 'member_delete_confirm.html', + {'member_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_add_many(request, *args, **kwargs): + """Render add multiple members page.""" return render(request, 'member_add_many.html', {}) @@ -139,15 +155,18 @@ def member_add_many(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_submit(request, *args, **kwargs): + """Add member based on data gained from member form.""" form = MemberForm(request.POST) if form.is_valid(): form.save() - logging.info("Saved new member to member register with the following info: {}".format(form)) + logging.info("Saved new member to member register" + "with the following info: {}".format(form)) notification = "{} {} {}.".format(_("Successfully added member"), form.cleaned_data['last_name'], form.cleaned_data['first_name']) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: return render(request, 'error.html', {'error': form.errors}) @@ -156,6 +175,7 @@ def member_submit(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_update(request, *args, **kwargs): + """Update member information.""" form = MemberForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -163,51 +183,68 @@ def member_update(request, *args, **kwargs): form = MemberForm(request.POST, instance=member) form.save() - logging.info("Updated member in member register with the following info: {}".format(form)) + logging.info( + "Updated member in member register with the following info: {}" + .format(form)) notification = "{} {} {}.".format(_("Successfully updated member"), member.last_name, member.first_name) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not update member object')}) + return render( + request, + 'error.html', + {'error': _('Could not update member object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def member_delete(request, *args, **kwargs): + """Delete member.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render(request, + 'error.html', {'error': _('No member id specified')}) try: member = Member.objects.get(id=id) notification = "{} {} {}.".format(_("Successfully deleted member"), member.last_name, member.first_name) member.delete() - logging.info("Delete member in member register with the following id: {}".format(id)) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + logging.info( + "Delete member in member register with the following id: {}" + .format(id)) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete member object')}) + return render(request, + 'error.html', + {'error': _('Could not delete member object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def member_edit(request, *args, **kwargs): + """Edit member information.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No member id specified')}) + return render( + request, 'error.html', {'error': _('No member id specified')}) else: member = Member.objects.get(id=i) form = MemberForm(instance=member) - return render(request, 'member_edit.html', {'member_id': i, 'form': form}) + return render( + request, 'member_edit.html', {'member_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_list(request, *args, **kwargs): + """List member applications not yet processed.""" applications = Request.objects.all() application_count = len(applications) table = RequestTable(applications, @@ -229,19 +266,25 @@ def application_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_edit(request, *args, **kwargs): + """Edit member request information.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render( + request, 'error.html', {'error': _('No application id specified')}) else: application = Request.objects.get(id=i) form = ApplicationForm(instance=application) - return render(request, 'application_edit.html', {'application_id': i, 'form': form}) + return render( + request, + 'application_edit.html', + {'application_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def application_accept(request, *args, **kwargs): + """Accept application.""" form = ApplicationForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -251,52 +294,75 @@ def application_accept(request, *args, **kwargs): member.save() application.delete() - logging.info("Accepted application in member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully accepted application"), str(application)) - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) + logging.info( + "Accepted application in member " + "register with the following info: {}" + .format(form)) + notification = "{} {}.".format(_("Successfully accepted application"), + str(application)) + return HttpResponseRedirect( + '/members/list?notification={}'.format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not accept application object')}) + return render(request, + 'error.html', + {'error': _('Could not accept application object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def application_delete(request, *args, **kwargs): + """Delete member application.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render( + request, 'error.html', {'error': _('No application id specified')}) try: application = Request.objects.get(id=id) - notification = "{} {}.".format(_("Successfully deleted application"), str(application)) + notification = "{} {}.".format(_("Successfully deleted application"), + str(application)) application.delete() - logging.info("Delete application in member register with the following id: {}".format(id)) - return HttpResponseRedirect('/members/applications?notification={}'.format(html.escape(notification))) + logging.info( + "Delete application in member register with the following id: {}" + .format(id)) + return HttpResponseRedirect( + '/members/applications?notification={}' + .format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete application object')}) + return render(request, + 'error.html', + {'error': _('Could not delete application object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def application_delete_confirm(request, *args, **kwargs): + """Confirm application deletion.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No application id specified')}) + return render(request, + 'error.html', + {'error': _('No application id specified')}) else: application = Request.objects.get(id=i) form = ApplicationForm(instance=application) - return render(request, 'application_delete_confirm.html', {'application_id': i, 'form': form}) + return render(request, + 'application_delete_confirm.html', + {'application_id': i, 'form': form}) @ensure_csrf_cookie def application_form(request, *args, **kwargs): + """Render member application form.""" return render(request, 'application_index.html', {}) @ensure_csrf_cookie def application_form_success(request, *args, **kwargs): + """Render application Successfully sent page.""" return render(request, 'application_success.html', {}) @@ -304,6 +370,7 @@ def application_form_success(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_list(request, *args, **kwargs): + """Render list of payments.""" payments = Payment.objects.all() table = PaymentTable(payments, @@ -326,6 +393,7 @@ def payment_list(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_add(request, *args, **kwargs): + """Render add payment form.""" form = PaymentForm() return render(request, 'payment_add.html', {'form': form}) @@ -334,13 +402,19 @@ def payment_add(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_submit(request, *args, **kwargs): + """Submit payment.""" form = PaymentForm(request.POST) if form.is_valid(): form.save() - logging.info("Saved new payment to member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully added payment for member"), - form.cleaned_data['member']) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Saved new payment to member register with the following info: {}" + .format(form)) + notification = "{} {}.".format( + _("Successfully added payment for member"), + form.cleaned_data['member']) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) else: return render(request, 'error.html', {'error': form.errors}) @@ -349,51 +423,71 @@ def payment_submit(request, *args, **kwargs): @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_edit(request, *args, **kwargs): + """Edit payment.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) else: payment = Payment.objects.get(id=i) form = PaymentForm(instance=payment) - return render(request, 'payment_edit.html', {'payment_id': i, 'form': form}) + return render(request, + 'payment_edit.html', + {'payment_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def payment_delete_confirm(request, *args, **kwargs): + """Render payment delete confirmation page.""" i = kwargs.pop('index', None) if i is None: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) else: payment = Payment.objects.get(id=i) form = PaymentForm(instance=payment) - return render(request, 'payment_delete_confirm.html', {'payment_id': i, 'form': form}) + return render(request, + 'payment_delete_confirm.html', + {'payment_id': i, 'form': form}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_delete(request, *args, **kwargs): + """Delete payment.""" try: id = request.POST['id'] except KeyError: - return render(request, 'error.html', {'error': _('No payment id specified')}) + return render(request, + 'error.html', + {'error': _('No payment id specified')}) try: payment = Payment.objects.get(id=id) - notification = "{} {}.".format(_("Successfully deleted payment"), str(payment)) + notification = "{} {}.".format( + _("Successfully deleted payment"), str(payment)) payment.delete() - logging.info("Delete payment '{}' in member register".format(str(payment))) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Delete payment '{}' in member register".format(str(payment))) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) except: - return render(request, 'error.html', {'error': _('Could not delete payment object')}) + return render(request, + 'error.html', + {'error': _('Could not delete payment object')}) @ensure_csrf_cookie @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def payment_update(request, *args, **kwargs): + """Update payment information.""" form = PaymentForm(request.POST) if form.is_valid(): id = request.POST['id'] @@ -401,17 +495,25 @@ def payment_update(request, *args, **kwargs): form = PaymentForm(request.POST, instance=payment) form.save() - logging.info("Updated member in member register with the following info: {}".format(form)) - notification = "{} {}.".format(_("Successfully updated payment"), str(payment)) - return HttpResponseRedirect('/members/payments?notification={}'.format(html.escape(notification))) + logging.info( + "Updated member in member register with the following info: {}" + .format(form)) + notification = "{} {}.".format( + _("Successfully updated payment"), str(payment)) + return HttpResponseRedirect( + '/members/payments?notification={}' + .format(html.escape(notification))) else: - return render(request, 'error.html', {'error': _('Could not update payment object')}) + return render(request, + 'error.html', + {'error': _('Could not update payment object')}) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def settings_page(request, *args, **kwargs): + """Render member app settings page.""" return render(request, 'settings.html', {}) @@ -419,30 +521,80 @@ def settings_page(request, *args, **kwargs): @require_http_methods(["POST"]) @permission_required('members.change_member', login_url='/login') def import_csv(request, *args, **kwargs): + """Get csv data imported to page and create members based on that.""" try: data = request.POST['textfield'] + payment_source = request.POST['payment_source'] except: - return render(request, 'error.html', {'error': _('Missing "textfield" POST request field')}) + return render(request, + 'error.html', + {'error': _('Missing "textfield" POST request field')}) - success = Member.from_csv(data) - if success: - logging.info('Imported CSV data:\n'.format(data)) - notification = "{}.".format(_("Successfully imported multiple members")) + try: + result = MemberForm.csv_to_models(data, payment_source=payment_source) + except CSVValidationError as ex: + logging.exception('Model validation error') + return error_view(request, ex.form_errors) + except Exception as ex: + logging.exception('Other error in CSV import') + return error_view(request, ex) + + member_table = MemberTable(result.members, + request=request, + exclude=['id', 'options'], + attrs={'class': 'table table-bordered table-hover'}) + + member_table_html = convert_table_to_html(member_table, request) + + payment_table = PaymentTable(result.payments, + request=request, + exclude=['id', 'options'], + attrs={'class': 'table table-bordered table-hover'}) + + payment_table_html = convert_table_to_html(payment_table, request) + + request.session['models'] = result + context = { + 'members': member_table_html, + 'payments': payment_table_html + } + return render(request, 'member_add_many_confirm.html', context) + + +@ensure_csrf_cookie +@require_http_methods(["POST"]) +@permission_required('members.change_member', login_url='/login') +def add_many_confirm(request, *args, **kwargs): + models = request.session['models'] + + try: + members, payments = models.members, models.payments + for member in members: + member.save() + + for payment in payments: + payment.save() + + msg = "Successfully imported {} members and {} payments." + notification = _(msg).format(len(members), len(payments)) return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) - else: - return render(request, 'error.html', {'error': _('Failed to import members')}) + except Exception as ex: + logging.exception('Failed to save models after "add many."') + return error_view(request, _('Failed to import members')) @ensure_csrf_cookie @require_http_methods(["GET"]) @permission_required('members.change_member', login_url='/login') def export_csv(request, *args, **kwargs): + """Export members as csv.""" response = HttpResponse() response['Content-type'] = 'text/csv' response['Accept'] = 'text/csv' response['Content-Disposition'] = 'filename; filename=members.csv' writer = csv.writer(response, csv.excel) - response.write(u'\ufeff'.encode('utf8')) # BOM (optional...Excel needs it to open UTF-8 file properly) + # BOM (optional...Excel needs it to open UTF-8 file properly) + response.write(u'\ufeff'.encode('utf8')) for obj in Member.objects.all(): data = obj.as_array() field_list = map(lambda d: str(d), data) @@ -452,55 +604,8 @@ def export_csv(request, *args, **kwargs): return response -@ensure_csrf_cookie -@require_http_methods(["GET"]) -@permission_required('members.change_member', login_url='/login') -def member_duplicates(request, *args, **kwargs): - conflicts = MemberConflict.objects.all() - context = { - 'conflicts': conflicts - } - - return render(request, 'member_duplicates.html', context) - - -@ensure_csrf_cookie -@require_http_methods(["POST"]) -@permission_required('members.change_member', login_url='/login') -def resolve_conflict(request, *args, **kwargs): - action = request.POST.get('action', None) - if action not in ['first', 'second', 'both']: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect action value'), action)}) - - id = request.POST.get('id', None) - if id is None: - return render(request, 'error.html', {'error': '{}: {}'.format(('Incorrect id value'), id)}) - - conflict = MemberConflict.objects.get(id=id) - first_member = conflict.first_member - second_member = conflict.second_member - - if action == 'first': - for payment in second_member.payments.all(): - payment.member = first_member - payment.save() - second_member.delete() - elif action == 'second': - for payment in first_member.payments.all(): - payment.member = second_member - payment.save() - first_member.delete() - - conflict.delete() - - if MemberConflict.objects.exists(): - return HttpResponseRedirect('/members/duplicates') - else: - notification = _('Successfully resolved all member conflicts.') - return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification))) - - def send_mail_wrapper(subject, message, email_to): + """Send mail to default email.""" send_mail(subject, message, settings.DEFAULT_EMAIL_FROM, @@ -510,6 +615,7 @@ def send_mail_wrapper(subject, message, email_to): @receiver(post_save, sender=Request) def email_on_request(sender, instance, created, **kwargs): + """Send email validation.""" if not settings.ENABLE_AUTOMATIC_EMAILS: return @@ -524,6 +630,7 @@ def email_on_request(sender, instance, created, **kwargs): @receiver(post_save, sender=Member) def email_on_accept(sender, instance, created, **kwargs): + """Send email to accepted member.""" if not settings.ENABLE_AUTOMATIC_EMAILS: return @@ -536,27 +643,10 @@ def email_on_accept(sender, instance, created, **kwargs): logging.error('Failed to send email to accepted member!') -def check_for_duplicates(instance): - name_candidates = Member.objects.filter(first_name=instance.first_name, - last_name=instance.last_name) - email_candidates = Member.objects.filter(email=instance.email) - - candidates = name_candidates | email_candidates - duplicates = candidates.exclude(id=instance.id) - - if len(duplicates) > 0: - conflict = MemberConflict(first_member=instance, - second_member=duplicates[0]) - conflict.save() - - -@receiver(post_save, sender=Member) -def duplicate_receiver(sender, instance, created, **kwargs): - check_for_duplicates(instance) - - # Can be used to retrieve single member information via REST API class MemberDetail(generics.RetrieveAPIView): + """Member detail rest API view.""" + queryset = Member.objects.all() serializer_class = MemberSerializer permission_classes = (permissions.IsAdminUser, ) diff --git a/setup.sh b/setup.sh index 8f71754..6ac4281 100755 --- a/setup.sh +++ b/setup.sh @@ -16,7 +16,7 @@ then USE_NPM="false" fi -$INTERACTIVE || echo "Running in non-interactive mode." && env +$INTERACTIVE || (echo "Running in non-interactive mode." && env) $INTERACTIVE && read -p "Are these programs installed? [y/n]" -n 1 -r || REPLY="y" echo "" diff --git a/sikweb/.ci-settings.py b/sikweb/.ci-settings.py index 8717ed4..043f3ce 100644 --- a/sikweb/.ci-settings.py +++ b/sikweb/.ci-settings.py @@ -1,3 +1,5 @@ +"""File containing CI settings.""" + from sikweb.default_settings import * DATABASES = { diff --git a/sikweb/settings-sample.py b/sikweb/settings-sample.py index 2013527..9ad9d7d 100644 --- a/sikweb/settings-sample.py +++ b/sikweb/settings-sample.py @@ -79,7 +79,7 @@ LOGGING = { # Application definition INSTALLED_APPS = [ - 'modeltranslation', # has to be before admin for translation admin stuff to work + 'modeltranslation', # has to be before admin for translation admin to work 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -105,7 +105,8 @@ NOSE_ARGS = [ '--with-coverage', '--cover-package=webapp,members,infoscreen', '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'members', 'migrations')), - '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'infoscreen', 'migrations')), + '--exclude-dir={}'.format(os.path.join(BASE_DIR, + 'infoscreen', 'migrations')), '--exclude-dir={}'.format(os.path.join(BASE_DIR, 'webapp', 'migrations')), ] @@ -182,16 +183,20 @@ else: AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'MinimumLengthValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'CommonPasswordValidator', }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + 'NAME': 'django.contrib.auth.password_validation.' + 'NumericPasswordValidator', }, ] diff --git a/sikweb/urls.py b/sikweb/urls.py index 0113b57..2e0793c 100644 --- a/sikweb/urls.py +++ b/sikweb/urls.py @@ -41,5 +41,6 @@ urlpatterns = [ # staticfiles default view for static files in development url(r'^static/(?P.*)$', static_views.serve), - url(r'^media/(?P.*)$', static_serve, {'document_root': settings.MEDIA_ROOT}), + url(r'^media/(?P.*)$', + static_serve, {'document_root': settings.MEDIA_ROOT}), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/webapp/admin.py b/webapp/admin.py index b532141..03faf69 100644 --- a/webapp/admin.py +++ b/webapp/admin.py @@ -1,3 +1,5 @@ +"""File containing webapp app admin registers.""" + from django.contrib import admin from webapp.models import Official, Role from webapp.models import Feed, Tag, BaseFeed, Event diff --git a/webapp/apps.py b/webapp/apps.py index 55fa92c..151670c 100644 --- a/webapp/apps.py +++ b/webapp/apps.py @@ -1,8 +1,13 @@ +"""Webapp app configurations.""" + from django.apps import AppConfig class WebappConfig(AppConfig): + """Webapp configurations.""" + name = 'webapp' def ready(self): + """Import translations.""" import webapp.translations diff --git a/webapp/forms.py b/webapp/forms.py new file mode 100644 index 0000000..0007eab --- /dev/null +++ b/webapp/forms.py @@ -0,0 +1,11 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from webapp.models import OhlhafvChallenge + + +class OhlhafvForm(forms.ModelForm): + + class Meta: + model = OhlhafvChallenge + fields = ['challenger', 'challenger_email', 'victim', 'victim_email', 'series', 'message'] diff --git a/webapp/migrations/0012_auto_20170913_1934.py b/webapp/migrations/0012_auto_20170913_1934.py new file mode 100644 index 0000000..4604c90 --- /dev/null +++ b/webapp/migrations/0012_auto_20170913_1934.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-13 16:34 +from __future__ import unicode_literals + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0011_auto_20170913_1841'), + ] + + operations = [ + migrations.CreateModel( + name='OhlhafvChallenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('challenger', models.CharField(max_length=255)), + ('victim', models.CharField(max_length=255)), + ('challenger_email', models.EmailField(max_length=254)), + ('victim_email', models.EmailField(max_length=254)), + ('series', models.CharField(choices=[('0.33 L', '0.33 L'), ('0.5 L', '0.5 L'), ('1.0 L', '1.0 L')], max_length=10)), + ('message', models.TextField()), + ], + ), + migrations.CreateModel( + name='Registration', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('options', django.contrib.postgres.fields.jsonb.JSONField()), + ], + ), + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AddField( + model_name='event', + name='registration', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='webapp.Registration'), + ), + ] diff --git a/webapp/migrations/0013_auto_20170920_1800.py b/webapp/migrations/0013_auto_20170920_1800.py new file mode 100644 index 0000000..d367aab --- /dev/null +++ b/webapp/migrations/0013_auto_20170920_1800.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 15:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0012_auto_20170913_1934'), + ] + + operations = [ + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=256, verbose_name='Name'), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='challenger', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='victim', + field=models.CharField(max_length=256), + ), + migrations.AlterField( + model_name='registration', + name='name', + field=models.CharField(max_length=256), + ), + ] diff --git a/webapp/migrations/0014_auto_20170920_1807.py b/webapp/migrations/0014_auto_20170920_1807.py new file mode 100644 index 0000000..633abb4 --- /dev/null +++ b/webapp/migrations/0014_auto_20170920_1807.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-09-20 15:07 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapp', '0013_auto_20170920_1800'), + ] + + operations = [ + migrations.AlterField( + model_name='baserole', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmyform', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='kaehmymessage', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='challenger', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='ohlhafvchallenge', + name='victim', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='registration', + name='name', + field=models.CharField(max_length=255), + ), + ] diff --git a/webapp/models.py b/webapp/models.py index 993702d..812d9f9 100644 --- a/webapp/models.py +++ b/webapp/models.py @@ -1,3 +1,5 @@ +"""Webapp app models.""" + from django.db import models from django.utils import timezone from datetime import timedelta @@ -7,18 +9,20 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from auditlog.registry import auditlog from phonenumber_field.modelfields import PhoneNumberField +from django.contrib.postgres.fields import JSONField class Tag(models.Model): + """Model for tag.""" + slug = models.SlugField(primary_key=True) name = models.CharField(max_length=127) icon = models.ImageField() class BaseFeed(models.Model): - ''' - model containing something showing on some info feed - ''' + """Model containing something showing on some info feed.""" + tags = models.ManyToManyField(Tag, related_name="feeds", blank=True) visible = models.BooleanField(default=True) title = models.CharField(max_length=255) @@ -27,40 +31,52 @@ class BaseFeed(models.Model): class Feed(BaseFeed): + """Model representing feed.""" publish_time = models.DateTimeField(default=timezone.now) autohide = models.DateTimeField(default=month_from_now) class Event(BaseFeed): + """Model for event.""" + start_time = models.DateTimeField(default=timezone.now) end_time = models.DateTimeField(default=timezone.now) + registration = models.ForeignKey( + 'Registration', on_delete=models.CASCADE, null=True) + + +class Registration(models.Model): + """Model for event registration.""" + + name = models.CharField(max_length=255) + email = models.EmailField() + options = JSONField() class BaseRole(models.Model): - ''' - Base model for occupations/roles - ''' - name = models.CharField(_('Name'), max_length=256) + """Base model for occupations/roles.""" + + name = models.CharField(_('Name'), max_length=255) is_board = models.BooleanField(_('Board member')) class PresetRole(BaseRole): - ''' - Model representing a preset occupation in the guild - ''' + """Model representing a preset occupation in the guild.""" + description = models.TextField(_('Description')) summary = models.TextField(_('Summary')) class PresetKaehmyRole(PresetRole): + """Model for kaehmy role.""" + form = models.ForeignKey('KaehmyForm', related_name='preset_roles') class CustomKaehmyRole(BaseRole): - ''' - Model representing a user-specified custom occupation - ''' + """Model representing a user-specified custom occupation.""" + form = models.ForeignKey('KaehmyForm', related_name='custom_roles') @@ -69,32 +85,41 @@ class MessageParent(models.Model): class KaehmyMessage(MessageParent): - ''' + """ Model representing a kaehmymessage. + Every message relates to certain kaehmyform or parent message. - ''' - name = models.CharField(_('Name'), max_length=256) + """ + + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) message = models.TextField(_('Message')) parent = models.ForeignKey('MessageParent', related_name='messages') class KaehmyForm(MessageParent): - ''' + """ Model representing a form for kaehmy. + Allows user to choose from existing roles or to create custom ones. - ''' - name = models.CharField(_('Name'), max_length=256) + """ + + name = models.CharField(_('Name'), max_length=255) email = models.EmailField(_('Email')) year = models.IntegerField(_('Year')) class Role(PresetRole): - ''' + """ + Model for Role. + Model representing an active or historical occupation - in an official's history - ''' + in an official's history. + """ + class Meta: + """Meta class for Role model.""" + verbose_name = _('Role') start_date = models.DateField(_('Start date')) @@ -103,15 +128,34 @@ class Role(PresetRole): class Official(User): - ''' - Model representing a guild official - ''' + """Model representing a guild official.""" + class Meta: + """Meta class for Official class.""" + verbose_name = _('Official') phone_number = PhoneNumberField(_('Phone number')) +# Ohlhafv +class OhlhafvChallenge(models.Model): + """Model containing all info about ohlhafv challenge.""" + + SERIES_CHOICES = ( + ('0.33 L', '0.33 L'), + ('0.5 L', '0.5 L'), + ('1.0 L', '1.0 L'), + ) + + challenger = models.CharField(max_length=255) + victim = models.CharField(max_length=255) + challenger_email = models.EmailField() + victim_email = models.EmailField() + series = models.CharField(choices=SERIES_CHOICES, max_length=10) + message = models.TextField() + + auditlog.register(Tag) auditlog.register(Feed) auditlog.register(Event) diff --git a/webapp/static/css/sik_header.css b/webapp/static/css/sik_header.css new file mode 100644 index 0000000..37685f6 --- /dev/null +++ b/webapp/static/css/sik_header.css @@ -0,0 +1,14 @@ +.header-content { + +} + +.header-content .logo { + +} + +.header-content .logo img { + display: block; + width: 100%; + height: auto; + margin: auto; +} diff --git a/webapp/static/img/logo_header.png b/webapp/static/img/logo_header.png new file mode 100644 index 0000000..f7491f3 Binary files /dev/null and b/webapp/static/img/logo_header.png differ diff --git a/webapp/tables.py b/webapp/tables.py new file mode 100644 index 0000000..186f011 --- /dev/null +++ b/webapp/tables.py @@ -0,0 +1,8 @@ +import django_tables2 as tables +from django.utils.translation import ugettext as _ +from webapp.models import OhlhafvChallenge + + +class OhlhafvTable(tables.Table): + class Meta: + model = OhlhafvChallenge diff --git a/webapp/templates/base.html b/webapp/templates/base.html index 16c8cce..d864e21 100644 --- a/webapp/templates/base.html +++ b/webapp/templates/base.html @@ -22,6 +22,9 @@ +
+ {% include "sik_header.html" %} +
{% include "navigation.html" %} {% block content %} diff --git a/webapp/templates/ohlhafv.html b/webapp/templates/ohlhafv.html new file mode 100644 index 0000000..05decc5 --- /dev/null +++ b/webapp/templates/ohlhafv.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% load bootstrap3 %} +{% load i18n %} + +{% block content %} +
+

{% trans "Ohlhafv" %}

+ +
+
{% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% endbuttons %} +
+
+
+{% endblock content %} diff --git a/webapp/templates/ohlhafv_list.html b/webapp/templates/ohlhafv_list.html new file mode 100644 index 0000000..a21e7e5 --- /dev/null +++ b/webapp/templates/ohlhafv_list.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} +{% load django_tables2 %} + +{% block content %} +
+ +
+

{% trans "All challenges" %}

+
+ +
+ {% trans "Total challenges:" %} {{ challenge_count }} +
+ + {{ table|safe }} +
+{% endblock content %} diff --git a/webapp/templates/sik_header.html b/webapp/templates/sik_header.html new file mode 100644 index 0000000..17ef251 --- /dev/null +++ b/webapp/templates/sik_header.html @@ -0,0 +1,8 @@ +{% load i18n %} + + +
+ +
diff --git a/webapp/tests.py b/webapp/tests.py index 7ce503c..1418e07 100644 --- a/webapp/tests.py +++ b/webapp/tests.py @@ -1,3 +1,5 @@ +"""Tests for webapp.""" + from django.test import TestCase # Create your tests here. diff --git a/webapp/translation.py b/webapp/translation.py index 678be16..9ee016d 100644 --- a/webapp/translation.py +++ b/webapp/translation.py @@ -1,22 +1,32 @@ +"""Translation classes.""" + from modeltranslation.translator import register, TranslationOptions from webapp.models import BaseFeed, Feed, Tag, Event @register(BaseFeed) class BaseFeedTranslationOptions(TranslationOptions): - fields = ('title', 'description', 'content') + """Class for base feed translation options.""" + + fields = ('title', 'description', 'content') @register(Feed) class FeedTranslationOptions(TranslationOptions): - fields = () + """Class for feed translation options.""" + + fields = () @register(Event) class EventTranslationOptions(TranslationOptions): - fields = () + """Class for event translation options.""" + + fields = () @register(Tag) class TagTranslationOptions(TranslationOptions): - fields = ('name',) + """Class for tag translation options.""" + + fields = ('name',) diff --git a/webapp/urls.py b/webapp/urls.py index 2dd136d..c517fbc 100644 --- a/webapp/urls.py +++ b/webapp/urls.py @@ -1,3 +1,5 @@ +"""Webapp urls.""" + from django.conf.urls import url from webapp.views import main_index @@ -5,6 +7,9 @@ from webapp.views import admin_index from webapp.views import login_view from webapp.views import logout_view from webapp.views import about_view +from webapp.views import ohlhafv_view +from webapp.views import ohlhafv_submit +from webapp.views import ohlhafv_list urlpatterns = [ # main @@ -17,4 +22,9 @@ urlpatterns = [ # git revision url(r'^about', about_view), + + # ohlhafv + url(r'^ohlhafv$', ohlhafv_view), + url(r'^ohlhafv/submit', ohlhafv_submit), + url(r'^ohlhafv/list', ohlhafv_list), ] diff --git a/webapp/utils.py b/webapp/utils.py index 0886e08..ceb83c7 100644 --- a/webapp/utils.py +++ b/webapp/utils.py @@ -1,6 +1,9 @@ +"""Webapp utils.""" + from django.utils import timezone from datetime import timedelta def month_from_now(): + """Return date one month from now.""" return timezone.now() + timedelta(days=30) diff --git a/webapp/views.py b/webapp/views.py index d8e0ac5..d398898 100644 --- a/webapp/views.py +++ b/webapp/views.py @@ -1,14 +1,21 @@ +"""Webapp views.""" + from django.shortcuts import render, redirect from django.contrib.auth import login, logout, authenticate from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import ensure_csrf_cookie +from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import permission_required from django.conf import settings import logging +from webapp.models import OhlhafvChallenge +from webapp.forms import OhlhafvForm +from webapp.tables import OhlhafvTable @require_http_methods(["GET"]) def main_index(request, *args, **kwargs): + """Render main page.""" return render(request, "main_index.html", {}) @@ -16,11 +23,13 @@ def main_index(request, *args, **kwargs): @ensure_csrf_cookie @permission_required('members.change_member', login_url='/login') def admin_index(request, *args, **kwargs): + """Render admin main page.""" return render(request, "admin_index.html", {}) @require_http_methods(["GET", "POST"]) def login_view(request, *args, **kwargs): + """Render login view.""" if request.method == "POST": uname = request.POST.get("username", None) pw = request.POST.get("passwd", None) @@ -29,7 +38,9 @@ def login_view(request, *args, **kwargs): login(request, user) original_site = request.GET.get("next", None) or "/" return redirect(original_site) - return render(request, "login.html", {"error": "☹ Kirjautuminen kosahti. Yritä uudelleen!"}) + return render(request, + "login.html", + {"error": "☹ Kirjautuminen kosahti. Yritä uudelleen!"}) # user got here by a get request user = request.user @@ -41,10 +52,51 @@ def login_view(request, *args, **kwargs): @require_http_methods(["POST"]) def logout_view(request, *args, **kwargs): + """Logout user and return to main page.""" logout(request) return redirect("/") @require_http_methods(["GET"]) def about_view(request, *args, **kwargs): + """Render about page.""" return render(request, "about.html", {}) + + +@require_http_methods(["GET"]) +def ohlhafv_view(request, *args, **kwargs): + form = OhlhafvForm() + return render(request, 'ohlhafv.html', {'form': form}) + + +@ensure_csrf_cookie +@require_http_methods(["POST"]) +def ohlhafv_submit(request, *args, **kwargs): + form = OhlhafvForm(request.POST) + if form.is_valid(): + form.save() + # return HttpResponseRedirect('/list/') + else: + pass + # return render(request, 'error.html', {'error': form.errors}) + return HttpResponseRedirect('/ohlhafv/list/') + + +@ensure_csrf_cookie +@require_http_methods(["GET"]) +def ohlhafv_list(request, *args, **kwargs): + challenges = OhlhafvChallenge.objects.all() + + table = OhlhafvTable(challenges, + request=request, + exclude=['id', 'challenger_email', 'victim_email'], + attrs={'class': 'table table-bordered table-hover'}) + + table.paginate(page=request.GET.get('page', 1), per_page=25) + table_html = table.as_html(request) + + context = { + 'table': table_html, + 'challenge_count': len(challenges), + } + return render(request, 'ohlhafv_list.html', context)