Merge branch 'develop' into 'master'

Develop

See merge request !46
This commit is contained in:
Jan Tuomi
2017-09-20 23:47:25 +03:00
46 changed files with 1031 additions and 299 deletions
+2 -2
View File
@@ -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():
+2
View File
@@ -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
+4
View File
@@ -1,5 +1,9 @@
"""Django apps configuration file."""
from django.apps import AppConfig
class InfoscreenConfig(AppConfig):
"""Infoscreen app configuration."""
name = 'infoscreen'
+26 -10
View File
@@ -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&center_coordinate={},{}"
.format(settings.HSL_USERHASH, location_coords[0], location_coords[1]))\
("https://api.reittiopas.fi/hsl/prod/?userhash={}"
"&request=stops_area&center_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)))
+96 -16
View File
@@ -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)
+13 -17
View File
@@ -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')
+2
View File
@@ -1,3 +1,5 @@
"""File containing infoscreen urls."""
from django.conf.urls import url
from infoscreen.views import index
+34 -11
View File
@@ -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)
+2
View File
@@ -1,3 +1,5 @@
"""Admin site registers for Members app."""
from django.contrib import admin
from members.models import Member, Request, Payment, MemberConflict
+4
View File
@@ -1,5 +1,9 @@
"""App configurations for members app."""
from django.apps import AppConfig
class MembersConfig(AppConfig):
"""Class for Members app configurations."""
name = 'members'
+81 -1
View File
@@ -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']
@@ -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'),
),
]
+28 -18
View File
@@ -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)
+8 -1
View File
@@ -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')
+19 -4
View File
@@ -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(
'<a class="data-table-button btn btn-primary" href="/members/edit/{{ record.id }}">' +
('<a class="data-table-button btn btn-primary" '
'href="/members/edit/{{ record.id }}">') +
_('Edit') +
'</a>',
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(
'<a class="data-table-button btn btn-primary" href="/members/edit_payment/{{ record.id }}">' +
('<a class="data-table-button btn btn-primary" '
'href="/members/edit_payment/{{ record.id }}">') +
_('Edit') +
'</a>',
verbose_name=_('Options')
)
class Meta:
"""Meta for payment table."""
model = Payment
class RequestTable(tables.Table):
"""Table for member applications."""
options = tables.TemplateColumn(
'<a class="data-table-button btn btn-primary" href="/members/edit_application/{{ record.id }}">' +
('<a class="data-table-button btn btn-primary" '
'href="/members/edit_application/{{ record.id }}">') +
_('Edit') +
'</a>',
verbose_name=_('Options')
)
class Meta:
"""Meta for request table."""
model = Request
+1 -1
View File
@@ -9,7 +9,7 @@
</div>
<div class="alert alert-danger">
{{ error }}
{{ error|safe }}
</div>
<div>
<button onclick="window.history.back();" class="btn btn-primary">{% trans "Back" %}</button>
+11 -2
View File
@@ -5,7 +5,7 @@
{% block content %}
<div>
<div>
<h3> Lisää useampi jäsen </h3>
<h3>{% trans "Add many members" %}</h3>
</div>
<div>
@@ -25,9 +25,18 @@
</div>
<form name="memberTextForm" action="/members/import_csv" method="POST">{% csrf_token %}
<div>
<div class="form-group">
<label>{% trans "Data" %}</label>
<textarea name="textfield" class="form-control large-textarea" placeholder="Teemu, Teekkari, teemu.teekkari@notmail.dom, Otaniemi, 0, 0"></textarea>
</div>
<div class="form-group">
<label>{% trans "Payment source" %}</label>
<select name="payment_source" class="form-control">
<option value="AYY">{% trans "AYY" %}</option>
<option value="bank_transfer">{% trans "Bank transfer" %}</option>
<option value="cash">{% trans "Cash payment" %}</option>
</select>
</div>
<div>
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
</div>
@@ -0,0 +1,26 @@
{% extends "members_base.html" %}
{% load i18n %}
{% block content %}
<div>
<div>
<h3>{% trans "Confirm adding these entries?" %}</h3>
</div>
<div class="form-group">
<label>{% trans "Members" %}</label>
{{ members|safe }}
</div>
<div class="form-group">
<label>{% trans "Payments" %}</label>
{{ payments|safe }}
</div>
</div>
<form name="memberTextForm" action="/members/add_many_confirm" method="POST">{% csrf_token %}
<div>
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
</div>
</form>
</div>
{% endblock content %}
-40
View File
@@ -1,40 +0,0 @@
{% extends "members_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block content %}
<div>
<div>
<h3>{% trans "Conflicting member entries" %}</h3>
</div>
<div>
<p>{% blocktrans %}Found conflicting member entries. Choose how to handle the problematic data.{% endblocktrans %}</p>
{% for conflict in conflicts %}
<div class="conflict-row">
<div class="col-md-6">
<table class="table readonly table-conflict bg-info" >
{{ conflict.first_member_form }}
</table>
</div>
<div class="col-md-6">
<table class="table readonly table-conflict bg-info" >
{{ conflict.second_member_form }}
</table>
</div>
<div>
<form action="/members/resolve_conflict" method="POST">{% csrf_token %}
<p>{% blocktrans %}Which one has the correct information for this member?{% endblocktrans %}</p>
<input type="hidden" name="id" value="{{ conflict.id }}">
<button type="submit" name="action" value="first" class="btn btn-primary">{% trans "Accept first and remove second" %}</button>
<button type="submit" name="action" value="second" class="btn btn-primary">{% trans "Accept second and remove first" %}</button>
<button type="submit" name="action" value="both" class="btn btn-primary">{% trans "Accept both as two members" %}</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock content %}
+9 -3
View File
@@ -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')
+6
View File
@@ -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'
+10 -10
View File
@@ -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<index>\d+)$', application_delete_confirm),
url(r'^delete_application_confirm/(?P<index>\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<index>\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<pk>\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)
]
+215 -125
View File
@@ -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, )
+1 -1
View File
@@ -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 ""
+2
View File
@@ -1,3 +1,5 @@
"""File containing CI settings."""
from sikweb.default_settings import *
DATABASES = {
+11 -6
View File
@@ -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',
},
]
+2 -1
View File
@@ -41,5 +41,6 @@ urlpatterns = [
# staticfiles default view for static files in development
url(r'^static/(?P<path>.*)$', static_views.serve),
url(r'^media/(?P<path>.*)$', static_serve, {'document_root': settings.MEDIA_ROOT}),
url(r'^media/(?P<path>.*)$',
static_serve, {'document_root': settings.MEDIA_ROOT}),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+2
View File
@@ -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
+5
View File
@@ -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
+11
View File
@@ -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']
@@ -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'),
),
]
@@ -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),
),
]
@@ -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),
),
]
+69 -25
View File
@@ -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)
+14
View File
@@ -0,0 +1,14 @@
.header-content {
}
.header-content .logo {
}
.header-content .logo img {
display: block;
width: 100%;
height: auto;
margin: auto;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+8
View File
@@ -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
+3
View File
@@ -22,6 +22,9 @@
</head>
<body>
<div class="header">
{% include "sik_header.html" %}
</div>
<div class="page-content">
{% include "navigation.html" %}
{% block content %}
+21
View File
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load bootstrap3 %}
{% load i18n %}
{% block content %}
<div>
<h3>{% trans "Ohlhafv" %}</h3>
<div id="input_form">
<form name="ohlhafvForm" action="/ohlhafv/submit/" method="post" class="form">{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">
{% trans "Challenge" %}
</button>
{% endbuttons %}
</form>
</div>
</div>
{% endblock content %}
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load django_tables2 %}
{% block content %}
<div>
<div>
<h2>{% trans "All challenges" %}</h2>
</div>
<div class="ohlhafv_count">
<span>{% trans "Total challenges:" %} {{ challenge_count }}</span>
</div>
{{ table|safe }}
</div>
{% endblock content %}
+8
View File
@@ -0,0 +1,8 @@
{% load i18n %}
<link rel="stylesheet" href="/static/css/sik_header.css">
<div class="header-content">
<div class="logo">
<a href="/"><img src="/static/img/logo_header.png" alt="Shiit ei kuvaa"></a>
</div>
</div>
+2
View File
@@ -1,3 +1,5 @@
"""Tests for webapp."""
from django.test import TestCase
# Create your tests here.
+14 -4
View File
@@ -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',)
+10
View File
@@ -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),
]
+3
View File
@@ -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)
+53 -1
View File
@@ -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)