Fix "add many" and csv import and export
This commit is contained in:
+7
-1
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from members.models import Member, Payment
|
||||
from members.models import Member, Payment, Request
|
||||
|
||||
|
||||
class MemberForm(forms.ModelForm):
|
||||
@@ -15,3 +15,9 @@ class PaymentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Payment
|
||||
fields = ['date', 'source', 'member']
|
||||
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Request
|
||||
fields = ['first_name', 'last_name', 'email', 'AYY', 'jas', 'POR']
|
||||
+60
-1
@@ -3,6 +3,11 @@ from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from datetime import datetime
|
||||
import csv
|
||||
import logging
|
||||
|
||||
memberlogger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseMember(models.Model):
|
||||
'''
|
||||
@@ -21,6 +26,41 @@ class BaseMember(models.Model):
|
||||
def __str__(self):
|
||||
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)
|
||||
|
||||
members = []
|
||||
for line in csv_reader:
|
||||
try:
|
||||
line = list(map(lambda x: x.strip(), line))
|
||||
print(line)
|
||||
|
||||
member = Member.from_array([
|
||||
line[0], line[1], line[2], line[3],
|
||||
bool(int(line[4])), bool(int(line[5]))
|
||||
])
|
||||
members.append(member)
|
||||
except:
|
||||
return False
|
||||
|
||||
for member in members:
|
||||
member.save()
|
||||
|
||||
return True
|
||||
|
||||
def as_array(self):
|
||||
return [
|
||||
self.first_name,
|
||||
self.last_name,
|
||||
self.email,
|
||||
self.POR,
|
||||
int(self.AYY),
|
||||
int(self.jas)
|
||||
]
|
||||
|
||||
|
||||
class Request(BaseMember):
|
||||
'''
|
||||
@@ -28,6 +68,11 @@ class Request(BaseMember):
|
||||
'''
|
||||
submitted = models.DateTimeField(default=timezone.now)
|
||||
|
||||
def to_member(self):
|
||||
member = Member.from_array(self.as_array())
|
||||
|
||||
return member
|
||||
|
||||
|
||||
class Payment(models.Model):
|
||||
'''
|
||||
@@ -50,4 +95,18 @@ class Member(BaseMember):
|
||||
'''
|
||||
Member model represets one member on the registry.
|
||||
'''
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
created = models.DateTimeField(default=timezone.now)
|
||||
|
||||
@staticmethod
|
||||
def from_array(array):
|
||||
if len(array) != 6:
|
||||
raise Exception("Invalid array length for member instantiation")
|
||||
|
||||
return Member.objects.create(
|
||||
first_name=array[0],
|
||||
last_name=array[1],
|
||||
email=array[2],
|
||||
POR=array[3],
|
||||
AYY=bool(array[4]),
|
||||
jas=bool(array[5]),
|
||||
)
|
||||
@@ -206,4 +206,10 @@ input {
|
||||
|
||||
.readonly {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.large-textarea {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 10rem !important;
|
||||
}
|
||||
@@ -1,40 +1,24 @@
|
||||
{% extends "members_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
<script>
|
||||
var memberId = {{ member_id }};
|
||||
</script>
|
||||
<div ng-controller="applEditController">
|
||||
<h1> Muokkaa hakemuksen jäsentietoja </h1>
|
||||
<div>
|
||||
<div>
|
||||
<h3>{% trans "Edit application" %}</h3>
|
||||
</div>
|
||||
|
||||
<div id="input_form">
|
||||
<form name="applicationForm">
|
||||
<div class="form-group">
|
||||
<label>Etunimi: </label>
|
||||
<input id="firstNameField" required type="text" placeholder="Sähkö" class="form-control" ng-model="member.first_name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sukunimi: </label>
|
||||
<input id="lastNameField" required type="text" placeholder="Insinööri" class="form-control" ng-model="member.last_name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Sähköposti: </label>
|
||||
<input id="emailField" required type="text" placeholder="sahko.insinoori@aalto.fi" class="form-control" ng-model="member.email"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>AYY jäsen: </label>
|
||||
<input type="checkbox" id="AYY" ng-model="member.AYY"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>JAS-listaan: </label>
|
||||
<input type="checkbox" id="jas" ng-model="member.jas"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Asuinkunta: </label>
|
||||
<input id="PORField" required type="text" placeholder="Otaniemi" class="form-control" ng-model="member.POR"></input>
|
||||
</div>
|
||||
<button class="btn btn-success" ng-click="applicationForm.$valid && sendappl()" type="submit" id="sendmember">Tallenna</button>
|
||||
<button class="btn btn-warning" ng-click="cancelappl()" type="submit" id="sendmember">Peruuta</button>
|
||||
<form name="applicationForm" action="/members/accept_application" method="post" class="form">{% csrf_token %}
|
||||
<input type="hidden" name="id" value="{{ application_id }}">
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Accept" %}
|
||||
</button>
|
||||
<a href="/members/delete_application_confirm/{{ application_id }}" class="btn btn-danger">{% trans "Decline" %}</a>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
{% extends "members_base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" style="align:middle;" ng-controller="addManyController">
|
||||
<div class="instructions">
|
||||
<div>
|
||||
<div>
|
||||
<h3> Lisää useampi jäsen </h3>
|
||||
<h5>
|
||||
Syötä jäsentiedot pilkuilla erotettuina formaatissa <b>Etunimi, Sukunimi, Sähköposti, Asuinkunta, AYY-jäsen(0 tai 1), JAS-listaan(0 tai 1)</b>
|
||||
<br>
|
||||
<br>
|
||||
Erota jäsenet rivinvaihdoilla toisistaan.
|
||||
</h5>
|
||||
<!--<img src="/static/img/members_example.png" alt="exampleImg" style="max-width:100%; height:auto; margin-bottom:10px; opacity:0.7;">-->
|
||||
</div>
|
||||
<form name="memberTextForm">
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<textarea ng-model="memberData" rows=5 cols=80 placeholder="Salli, Vahvonen, salli.vahvonen@notmail.dom, Kerava, 0, 0" style="border:solid 3px #c9c9c9; transition:box-shadow 0.3s, border 0.3s; align:middle;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
<button ng-click="sendCSV()" type="submit" class="btn btn-success" id="sendMembers">Lähetä</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer-div"/>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
Enter member information in CSV format, separate members on separate lines.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
{% blocktrans %}
|
||||
first_name, last_name, email_address and place_of_origin should be given string values.
|
||||
ayy_member and jas_recipient should be given the value 0 (off) or 1 (on).
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<h4>{% trans "Syntax" %}</h4>
|
||||
<pre>first_name, last_name, email_address, place_of_origin, ayy_member, jas_recipient</pre>
|
||||
|
||||
</div>
|
||||
<form name="memberTextForm" action="/members/import_csv" method="POST">{% csrf_token %}
|
||||
<div>
|
||||
<textarea name="textfield" class="form-control large-textarea" placeholder="Teemu, Teekkari, teemu.teekkari@notmail.dom, Otaniemi, 0, 0"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -20,49 +20,7 @@
|
||||
{{ table|safe }}
|
||||
|
||||
<div>
|
||||
<a id="download-csv" class="btn btn-info">{% trans "Download CSV" %}</a>
|
||||
</div>
|
||||
|
||||
<form action="/members/list" method="POST" id="collapse-filters" class="collapse filter-form">
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<input class="form-control" type="text" id="search-filter" placeholder="{% trans "Search" %}" >
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Added after" %}</h5>
|
||||
<input type="datetime-local" id="addedAfterDatePicker">
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Added before" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="addedBeforeDatePicker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Paid after" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="paidAfterDatePicker">
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Paid before" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="paidBeforeDatePicker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<input type="button" value="{% trans "Filter" %}" class="filter-button btn btn-success">
|
||||
<input type="button" value="{% trans "Reset" %}" class="filter-button btn btn-warning">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a id="filter-collapser" href="#collapse-filters" data-toggle="collapse" class="btn btn-info">
|
||||
{% trans "Show filters" %}
|
||||
</a>
|
||||
<a href="/members/export_csv" class="btn btn-info">{% trans "Download CSV" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -15,51 +15,5 @@
|
||||
{% endif %}
|
||||
|
||||
{{ table|safe }}
|
||||
|
||||
<div>
|
||||
<a id="download-csv" class="btn btn-info">{% trans "Download CSV" %}</a>
|
||||
</div>
|
||||
|
||||
<form action="/members/list" method="POST" id="collapse-filters" class="collapse filter-form">
|
||||
<div class="filter-row">
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<input class="form-control" type="text" id="search-filter" placeholder="{% trans "Search" %}" >
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Added after" %}</h5>
|
||||
<input type="datetime-local" id="addedAfterDatePicker">
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Added before" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="addedBeforeDatePicker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Paid after" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="paidAfterDatePicker">
|
||||
</div>
|
||||
<div class="filter-field">
|
||||
<h5>{% trans "Paid before" %}</h5>
|
||||
<input type="datetime-local" class="filter-field" id="paidBeforeDatePicker">
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<div class="filter-field">
|
||||
<input type="button" value="{% trans "Filter" %}" class="filter-button btn btn-success">
|
||||
<input type="button" value="{% trans "Reset" %}" class="filter-button btn btn-warning">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<a id="filter-collapser" href="#collapse-filters" data-toggle="collapse" class="btn btn-info">
|
||||
{% trans "Show filters" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
+13
-1
@@ -2,7 +2,8 @@ from django.conf.urls import url
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
# members
|
||||
from members.views import member_list, payment_add, payment_submit
|
||||
from members.views import member_list, payment_add, payment_submit, application_delete_confirm, application_delete, \
|
||||
application_accept, import_csv, export_csv
|
||||
from members.views import settings_page, payment_edit
|
||||
from members.views import payment_delete_confirm
|
||||
from members.views import payment_delete, payment_update
|
||||
@@ -54,6 +55,8 @@ urlpatterns = [
|
||||
url(r'^submit_payment$', payment_submit),
|
||||
url(r'^update_payment$', payment_update),
|
||||
url(r'^delete_payment$', payment_delete),
|
||||
url(r'^accept_application$', application_accept),
|
||||
url(r'^delete_application$', application_delete),
|
||||
|
||||
# the actual member application form
|
||||
url(r'^application/$', application_form),
|
||||
@@ -61,6 +64,9 @@ urlpatterns = [
|
||||
# success page for the application
|
||||
url(r'^application/success$', application_form_success),
|
||||
|
||||
# delete confirmation view for applications
|
||||
url(r'^delete_application_confirm/(?P<index>\d+)$', application_delete_confirm),
|
||||
|
||||
# list all payment events
|
||||
url(r'^payments$', payment_list),
|
||||
|
||||
@@ -76,6 +82,12 @@ urlpatterns = [
|
||||
# settings page
|
||||
url(r'^settings$', settings_page),
|
||||
|
||||
# send CSV member data by POST
|
||||
url(r'^import_csv', import_csv),
|
||||
|
||||
# download CSV member data
|
||||
url(r'^export_csv', export_csv),
|
||||
|
||||
# favourite icon
|
||||
url(r'^favicon\.ico$', favicon_view),
|
||||
]
|
||||
|
||||
+71
-21
@@ -11,9 +11,10 @@ import json
|
||||
import requests
|
||||
import logging
|
||||
import html
|
||||
import csv
|
||||
|
||||
from members.models import Member, Request, Payment
|
||||
from members.forms import MemberForm, PaymentForm
|
||||
from members.forms import MemberForm, PaymentForm, ApplicationForm
|
||||
|
||||
# Logger function, you can use the same idea when implementing other loggers to other apps
|
||||
from members.tables import MemberTable, PaymentTable, RequestTable
|
||||
@@ -169,7 +170,7 @@ def member_delete(request, *args, **kwargs):
|
||||
try:
|
||||
id = request.POST['id']
|
||||
except KeyError:
|
||||
return HttpResponse(401)
|
||||
return render(request, 'error.html', {'error': _('No member id specified')})
|
||||
|
||||
try:
|
||||
member = Member.objects.get(id=id)
|
||||
@@ -224,7 +225,61 @@ def application_edit(request, *args, **kwargs):
|
||||
if i is None:
|
||||
return render(request, 'error.html', {'error': _('No application id specified')})
|
||||
else:
|
||||
return render(request, 'application_edit.html', {'member_id': i})
|
||||
application = Request.objects.get(id=i)
|
||||
form = ApplicationForm(instance=application)
|
||||
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):
|
||||
form = ApplicationForm(request.POST)
|
||||
if form.is_valid():
|
||||
id = request.POST['id']
|
||||
application = Request.objects.get(id=id)
|
||||
|
||||
member = application.to_member()
|
||||
member.save()
|
||||
application.delete()
|
||||
|
||||
memberlogger.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')})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@permission_required('members.change_member', login_url='/login')
|
||||
def application_delete(request, *args, **kwargs):
|
||||
try:
|
||||
id = request.POST['id']
|
||||
except KeyError:
|
||||
return render(request, 'error.html', {'error': _('No application id specified')})
|
||||
|
||||
try:
|
||||
application = Request.objects.get(id=id)
|
||||
notification = "{} {}.".format(_("Successfully deleted application"), str(application))
|
||||
application.delete()
|
||||
memberlogger.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')})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
@permission_required('members.change_member', login_url='/login')
|
||||
def application_delete_confirm(request, *args, **kwargs):
|
||||
i = kwargs.pop('index', None)
|
||||
if i is None:
|
||||
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})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -315,7 +370,7 @@ def payment_delete(request, *args, **kwargs):
|
||||
try:
|
||||
id = request.POST['id']
|
||||
except KeyError:
|
||||
return HttpResponse(401)
|
||||
return render(request, 'error.html', {'error': _('No payment id specified')})
|
||||
|
||||
try:
|
||||
payment = Payment.objects.get(id=id)
|
||||
@@ -355,29 +410,25 @@ def settings_page(request, *args, **kwargs):
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@permission_required('members.change_member', login_url='/login')
|
||||
def csv_import(request, *args, **kwargs):
|
||||
data = request.body.decode("utf-8")
|
||||
|
||||
def import_csv(request, *args, **kwargs):
|
||||
try:
|
||||
payload = json.loads(data)
|
||||
data = request.POST['textfield']
|
||||
except:
|
||||
return HttpResponse(json.dumps({'error': 'Malformed request'}), 400)
|
||||
return render(request, 'error.html', {'error': _('Missing "textfield" POST request field')})
|
||||
|
||||
resp_data = Member.import_csv(payload['csv'])
|
||||
resp = HttpResponse(json.dumps(resp_data))
|
||||
if resp_data['status'] == 'failure':
|
||||
resp.status_code = 400
|
||||
memberlogger.warning('POST request failed with status code {}'.format(resp.status_code))
|
||||
|
||||
return resp
|
||||
success = Member.from_csv(data)
|
||||
if success:
|
||||
memberlogger.info('Imported CSV data:\n'.format(data))
|
||||
notification = "{}.".format(_("Successfully imported multiple members"))
|
||||
return HttpResponseRedirect('/members/list?notification={}'.format(html.escape(notification)))
|
||||
else:
|
||||
return render(request, 'error.html', {'error': _('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):
|
||||
import csv
|
||||
|
||||
response = HttpResponse()
|
||||
response['Content-type'] = 'text/csv'
|
||||
response['Accept'] = 'text/csv'
|
||||
@@ -385,9 +436,8 @@ def export_csv(request, *args, **kwargs):
|
||||
writer = csv.writer(response, csv.excel)
|
||||
response.write(u'\ufeff'.encode('utf8')) # BOM (optional...Excel needs it to open UTF-8 file properly)
|
||||
for obj in Member.objects.all():
|
||||
data = obj.get_dict()
|
||||
field_list = map(lambda s: str(data[s]),
|
||||
['id', 'first_name', 'last_name', 'email', 'POR', 'AYY', 'jas', 'created', 'paid'])
|
||||
data = obj.as_array()
|
||||
field_list = map(lambda d: str(d), data)
|
||||
|
||||
writer.writerow(field_list)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user