@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Django apps configuration file."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class InfoscreenConfig(AppConfig):
|
||||
"""Infoscreen app configuration."""
|
||||
|
||||
name = 'infoscreen'
|
||||
|
||||
+26
-10
@@ -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)))
|
||||
|
||||
+96
-16
@@ -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
@@ -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')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""File containing infoscreen urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from infoscreen.views import index
|
||||
|
||||
+34
-11
@@ -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)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Admin site registers for Members app."""
|
||||
|
||||
from django.contrib import admin
|
||||
from members.models import Member, Request, Payment, MemberConflict
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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')
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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, )
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""File containing CI settings."""
|
||||
|
||||
from sikweb.default_settings import *
|
||||
|
||||
DATABASES = {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
@@ -22,6 +22,9 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
{% include "sik_header.html" %}
|
||||
</div>
|
||||
<div class="page-content">
|
||||
{% include "navigation.html" %}
|
||||
{% block content %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Tests for webapp."""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
+14
-4
@@ -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',)
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user