finance/admin: validate IBAN and show EPC-QR code for transactions #94

Merged
christian.merten merged 12 commits from MK/finance_qr_codes into main 12 months ago

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-01 16:23+0100\n" "POT-Creation-Date: 2024-12-28 01:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,12 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: finance/admin.py:76 #: finance/admin.py:84
#, python-format #, python-format
msgid "%(name)s is already submitted." msgid "%(name)s is already submitted."
msgstr "%(name)s ist bereits eingereicht." msgstr "%(name)s ist bereits eingereicht."
#: finance/admin.py:82 #: finance/admin.py:90
#, python-format #, python-format
msgid "" msgid ""
"Successfully submited %(name)s. The finance department will notify the " "Successfully submited %(name)s. The finance department will notify the "
@ -32,23 +32,23 @@ msgstr ""
"Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich " "Rechnung %(name)s erfolgreich eingereicht. Das Finanzreferat wird auf dich "
"sobald wie möglich zukommen." "sobald wie möglich zukommen."
#: finance/admin.py:85 #: finance/admin.py:93
msgid "Submit statement" msgid "Submit statement"
msgstr "Rechnung einreichen" msgstr "Rechnung einreichen"
#: finance/admin.py:162 #: finance/admin.py:177
#, python-format #, python-format
msgid "%(name)s is not yet submitted." msgid "%(name)s is not yet submitted."
msgstr "%(name)s ist noch nicht eingereicht." msgstr "%(name)s ist noch nicht eingereicht."
#: finance/admin.py:169 #: finance/admin.py:184
#, python-format #, python-format
msgid "An error occured while trying to confirm %(name)s. Please try again." msgid "An error occured while trying to confirm %(name)s. Please try again."
msgstr "" msgstr ""
"Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es " "Beim Abwickeln von %(name)s ist ein Fehler aufgetreten. Bitte versuche es "
"erneut." "erneut."
#: finance/admin.py:173 #: finance/admin.py:188
#, python-format #, python-format
msgid "" msgid ""
"Successfully confirmed %(name)s. I hope you executed the associated " "Successfully confirmed %(name)s. I hope you executed the associated "
@ -57,11 +57,11 @@ msgstr ""
"Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen " "Erfolgreich %(name)s abgewickelt. Ich hoffe du hast die zugehörigen "
"Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern." "Überweisungen ausgeführt, ich werde dich nicht nochmal erinnern."
#: finance/admin.py:180 #: finance/admin.py:195
msgid "Statement confirmed" msgid "Statement confirmed"
msgstr "Abrechnung abgewickelt" msgstr "Abrechnung abgewickelt"
#: finance/admin.py:186 #: finance/admin.py:201
msgid "" msgid ""
"Transactions do not match the covered expenses. Please correct the mistakes " "Transactions do not match the covered expenses. Please correct the mistakes "
"listed below." "listed below."
@ -69,19 +69,19 @@ msgstr ""
"Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte " "Überweisungen stimmen nicht mit den übernommenen Kosten überein. Bitte "
"korrigiere die unten aufgeführten Fehler." "korrigiere die unten aufgeführten Fehler."
#: finance/admin.py:191 #: finance/admin.py:206
msgid "Some transactions have no ledger configured. Please fill in the gaps." msgid "Some transactions have no ledger configured. Please fill in the gaps."
msgstr "" msgstr ""
"Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach." "Manche Überweisungen haben kein Geldtopf eingestellt. Bitte trage das nach."
#: finance/admin.py:200 #: finance/admin.py:215
#, python-format #, python-format
msgid "Successfully rejected %(name)s. The requestor can reapply, when needed." msgid "Successfully rejected %(name)s. The requestor can reapply, when needed."
msgstr "" msgstr ""
"Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut " "Die Rechnung %(name)s wurde abgelehnt. Die Person kann die Rechnung erneut "
"einstellen, wenn es benötigt wird." "einstellen, wenn es benötigt wird."
#: finance/admin.py:207 #: finance/admin.py:222
#, python-format #, python-format
msgid "" msgid ""
"%(name)s already has transactions. Please delete them first, if you want to " "%(name)s already has transactions. Please delete them first, if you want to "
@ -90,12 +90,12 @@ msgstr ""
"%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du " "%(name)s hat bereits Überweisungen. Bitte lösche diese zunächst, bevor du "
"neue generierst." "neue generierst."
#: finance/admin.py:212 #: finance/admin.py:227
#, python-format #, python-format
msgid "Successfully generated transactions for %(name)s" msgid "Successfully generated transactions for %(name)s"
msgstr "Automatisch Überweisungsträger für %(name)s generiert." msgstr "Automatisch Überweisungsträger für %(name)s generiert."
#: finance/admin.py:215 #: finance/admin.py:230
#, python-format #, python-format
msgid "" msgid ""
"Error while generating transactions for %(name)s. Do all bills have a payer?" "Error while generating transactions for %(name)s. Do all bills have a payer?"
@ -103,28 +103,28 @@ msgstr ""
"Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle " "Fehler beim Erzeugen der Überweisungsträger für %(name)s. Sind für alle "
"Quittungen eine bezahlende Person eingestellt? " "Quittungen eine bezahlende Person eingestellt? "
#: finance/admin.py:218 #: finance/admin.py:233
msgid "View submitted statement" msgid "View submitted statement"
msgstr "Eingereichte Abrechnung einsehen" msgstr "Eingereichte Abrechnung einsehen"
#: finance/admin.py:230 #: finance/admin.py:245
#, python-format #, python-format
msgid "Successfully reduced transactions for %(name)s." msgid "Successfully reduced transactions for %(name)s."
msgstr "Überweisungsträger für %(name)s minimiert." msgstr "Überweisungsträger für %(name)s minimiert."
#: finance/admin.py:274 #: finance/admin.py:293
#, python-format #, python-format
msgid "%(name)s is not yet confirmed." msgid "%(name)s is not yet confirmed."
msgstr "%(name)s ist noch nicht bestätigt." msgstr "%(name)s ist noch nicht bestätigt."
#: finance/admin.py:283 #: finance/admin.py:302
#, python-format #, python-format
msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing." msgid "Successfully unconfirmed %(name)s. I hope you know what you are doing."
msgstr "" msgstr ""
"Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt " "Erfolgreich die Bestätigung von %(name)s zurückgenommen. Ich hoffe du weißt "
"was du machst." "was du machst."
#: finance/admin.py:288 finance/templates/admin/unconfirm_statement.html:26 #: finance/admin.py:307 finance/templates/admin/unconfirm_statement.html:26
msgid "Unconfirm statement" msgid "Unconfirm statement"
msgstr "Bestätigung zurücknehmen" msgstr "Bestätigung zurücknehmen"
@ -132,185 +132,185 @@ msgstr "Bestätigung zurücknehmen"
msgid "Finance" msgid "Finance"
msgstr "Finanzen" msgstr "Finanzen"
#: finance/models.py:21 #: finance/models.py:24
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
#: finance/models.py:27 finance/models.py:472 finance/models.py:496 #: finance/models.py:30 finance/models.py:484 finance/models.py:547
#: finance/templates/admin/confirmed_statement.html:38 #: finance/templates/admin/confirmed_statement.html:40
#: finance/templates/admin/overview_submitted_statement.html:100 #: finance/templates/admin/overview_submitted_statement.html:100
msgid "Ledger" msgid "Ledger"
msgstr "Geldtopf" msgstr "Geldtopf"
#: finance/models.py:28 #: finance/models.py:31
msgid "Ledgers" msgid "Ledgers"
msgstr "Geldtöpfe" msgstr "Geldtöpfe"
#: finance/models.py:48 finance/models.py:415 finance/models.py:495 #: finance/models.py:51 finance/models.py:420 finance/models.py:546
msgid "Short description" msgid "Short description"
msgstr "Kurzbeschreibung" msgstr "Kurzbeschreibung"
#: finance/models.py:51 finance/models.py:416 #: finance/models.py:54 finance/models.py:421
msgid "Explanation" msgid "Explanation"
msgstr "Erklärung" msgstr "Erklärung"
#: finance/models.py:53 #: finance/models.py:56
msgid "Associated excursion" msgid "Associated excursion"
msgstr "Zugehörige Ausfahrt" msgstr "Zugehörige Ausfahrt"
#: finance/models.py:58 #: finance/models.py:61
msgid "Price per night" msgid "Price per night"
msgstr "Preis pro Nacht" msgstr "Preis pro Nacht"
#: finance/models.py:60 #: finance/models.py:63
msgid "Submitted" msgid "Submitted"
msgstr "Eingericht" msgstr "Eingericht"
#: finance/models.py:61 #: finance/models.py:64
msgid "Submitted on" msgid "Submitted on"
msgstr "Eingereicht am" msgstr "Eingereicht am"
#: finance/models.py:62 #: finance/models.py:65
msgid "Confirmed" msgid "Confirmed"
msgstr "Abgewickelt" msgstr "Abgewickelt"
#: finance/models.py:63 finance/models.py:479 #: finance/models.py:66 finance/models.py:491
msgid "Paid on" msgid "Paid on"
msgstr "Bezahlt am" msgstr "Bezahlt am"
#: finance/models.py:65 #: finance/models.py:68
msgid "Created by" msgid "Created by"
msgstr "Erstellt von" msgstr "Erstellt von"
#: finance/models.py:70 #: finance/models.py:73
msgid "Submitted by" msgid "Submitted by"
msgstr "Eingereicht von" msgstr "Eingereicht von"
#: finance/models.py:75 finance/models.py:480 #: finance/models.py:78 finance/models.py:492
msgid "Authorized by" msgid "Authorized by"
msgstr "Autorisiert von" msgstr "Autorisiert von"
#: finance/models.py:82 finance/models.py:414 finance/models.py:475 #: finance/models.py:85 finance/models.py:419 finance/models.py:487
msgid "Statement" msgid "Statement"
msgstr "Abrechnung" msgstr "Abrechnung"
#: finance/models.py:83 #: finance/models.py:86
msgid "Statements" msgid "Statements"
msgstr "Abrechnungen" msgstr "Abrechnungen"
#: finance/models.py:98 #: finance/models.py:101
#, python-format #, python-format
msgid "Statement: %(excursion)s" msgid "Statement: %(excursion)s"
msgstr "Abrechnung: %(excursion)s" msgstr "Abrechnung: %(excursion)s"
#: finance/models.py:150 #: finance/models.py:153
msgid "Ready to confirm" msgid "Ready to confirm"
msgstr "Bereit zur Abwicklung" msgstr "Bereit zur Abwicklung"
#: finance/models.py:194 #: finance/models.py:197
#, python-format #, python-format
msgid "Compensation for %(excu)s" msgid "Compensation for %(excu)s"
msgstr "Entschädigung für %(excu)s" msgstr "Entschädigung für %(excu)s"
#: finance/models.py:327 #: finance/models.py:330
#: finance/templates/admin/overview_submitted_statement.html:78 #: finance/templates/admin/overview_submitted_statement.html:78
msgid "Total" msgid "Total"
msgstr "Gesamtbetrag" msgstr "Gesamtbetrag"
#: finance/models.py:369 #: finance/models.py:374
msgid "Statement in preparation" msgid "Statement in preparation"
msgstr "Abrechnung in Vorbereitung" msgstr "Abrechnung in Vorbereitung"
#: finance/models.py:370 #: finance/models.py:375
msgid "Statements in preparation" msgid "Statements in preparation"
msgstr "Abrechnungen in Vorbereitung" msgstr "Abrechnungen in Vorbereitung"
#: finance/models.py:389 #: finance/models.py:394
msgid "Submitted statement" msgid "Submitted statement"
msgstr "Eingereichte Abrechnung" msgstr "Eingereichte Abrechnung"
#: finance/models.py:390 #: finance/models.py:395
msgid "Submitted statements" msgid "Submitted statements"
msgstr "Eingereichte Abrechnungen" msgstr "Eingereichte Abrechnungen"
#: finance/models.py:406 #: finance/models.py:411
msgid "Paid statement" msgid "Paid statement"
msgstr "Bezahlte Abrechnung" msgstr "Bezahlte Abrechnung"
#: finance/models.py:407 #: finance/models.py:412
msgid "Paid statements" msgid "Paid statements"
msgstr "Bezahlte Abrechnungen" msgstr "Bezahlte Abrechnungen"
#: finance/models.py:418 finance/models.py:432 finance/models.py:469 #: finance/models.py:423 finance/models.py:444 finance/models.py:481
#: finance/templates/admin/confirmed_statement.html:36 #: finance/templates/admin/confirmed_statement.html:38
#: finance/templates/admin/overview_submitted_statement.html:31 #: finance/templates/admin/overview_submitted_statement.html:31
#: finance/templates/admin/overview_submitted_statement.html:98 #: finance/templates/admin/overview_submitted_statement.html:98
msgid "Amount" msgid "Amount"
msgstr "Betrag" msgstr "Betrag"
#: finance/models.py:419 #: finance/models.py:424
msgid "Paid by" msgid "Paid by"
msgstr "Bezahlt von" msgstr "Bezahlt von"
#: finance/models.py:421 #: finance/models.py:426
msgid "Covered" msgid "Covered"
msgstr "Übernommen" msgstr "Übernommen"
#: finance/models.py:422 #: finance/models.py:427
msgid "Refunded" msgid "Refunded"
msgstr "Ausgezahlt" msgstr "Ausgezahlt"
#: finance/models.py:424 #: finance/models.py:429
msgid "Proof" msgid "Proof"
msgstr "Beleg" msgstr "Beleg"
#: finance/models.py:435 finance/models.py:442 finance/models.py:455 #: finance/models.py:447 finance/models.py:454 finance/models.py:467
msgid "Bill" msgid "Bill"
msgstr "Ausgabe" msgstr "Ausgabe"
#: finance/models.py:436 finance/models.py:443 finance/models.py:456 #: finance/models.py:448 finance/models.py:455 finance/models.py:468
#: finance/templates/admin/overview_submitted_statement.html:26 #: finance/templates/admin/overview_submitted_statement.html:26
msgid "Bills" msgid "Bills"
msgstr "Ausgaben" msgstr "Ausgaben"
#: finance/models.py:468 finance/templates/admin/confirmed_statement.html:37 #: finance/models.py:480 finance/templates/admin/confirmed_statement.html:39
#: finance/templates/admin/overview_submitted_statement.html:99 #: finance/templates/admin/overview_submitted_statement.html:99
msgid "Reference" msgid "Reference"
msgstr "Verwendungszweck" msgstr "Verwendungszweck"
#: finance/models.py:470 #: finance/models.py:482
msgid "Recipient" msgid "Recipient"
msgstr "Empfänger" msgstr "Empfänger"
#: finance/models.py:478 #: finance/models.py:490
msgid "Paid" msgid "Paid"
msgstr "Bezahlt" msgstr "Bezahlt"
#: finance/models.py:490 #: finance/models.py:541
msgid "Transaction" msgid "Transaction"
msgstr "Überweisung" msgstr "Überweisung"
#: finance/models.py:491 #: finance/models.py:542
#: finance/templates/admin/overview_submitted_statement.html:84 #: finance/templates/admin/overview_submitted_statement.html:84
msgid "Transactions" msgid "Transactions"
msgstr "Überweisungen" msgstr "Überweisungen"
#: finance/templates/admin/confirmed_statement.html:17 #: finance/templates/admin/confirmed_statement.html:19
#: finance/templates/admin/overview_submitted_statement.html:17 #: finance/templates/admin/overview_submitted_statement.html:17
#: finance/templates/admin/submit_statement.html:17 #: finance/templates/admin/submit_statement.html:17
#: finance/templates/admin/unconfirm_statement.html:17 #: finance/templates/admin/unconfirm_statement.html:17
msgid "Home" msgid "Home"
msgstr "Start" msgstr "Start"
#: finance/templates/admin/confirmed_statement.html:21 #: finance/templates/admin/confirmed_statement.html:23
msgid "Paiment" msgid "Paiment"
msgstr "Bezahlung" msgstr "Bezahlung"
#: finance/templates/admin/confirmed_statement.html:26 #: finance/templates/admin/confirmed_statement.html:28
msgid "Paying statement" msgid "Paying statement"
msgstr "Rechnung bezahlen" msgstr "Rechnung bezahlen"
#: finance/templates/admin/confirmed_statement.html:29 #: finance/templates/admin/confirmed_statement.html:31
msgid "" msgid ""
"The statement is valid. Please execute the following transactions and then " "The statement is valid. Please execute the following transactions and then "
"proceed by finalizing the confirmation." "proceed by finalizing the confirmation."
@ -318,15 +318,32 @@ msgstr ""
"Die Abrechnung ist gültig. Bitte führe die folgenden Überweisungen aus und " "Die Abrechnung ist gültig. Bitte führe die folgenden Überweisungen aus und "
"fahre dann fort, indem du die Abwicklung bestätigst." "fahre dann fort, indem du die Abwicklung bestätigst."
#: finance/templates/admin/confirmed_statement.html:35 #: finance/templates/admin/confirmed_statement.html:37
msgid "IBAN" msgid "IBAN"
msgstr "IBAN" msgstr "IBAN"
#: finance/templates/admin/confirmed_statement.html:66 #: finance/templates/admin/confirmed_statement.html:41
msgid "QR Code"
msgstr "QR Code"
#: finance/templates/admin/confirmed_statement.html:61
#: finance/templates/admin/confirmed_statement.html:98
msgid "Show"
msgstr "Anzeigen"
#: finance/templates/admin/confirmed_statement.html:86
msgid "No QR code can be displayed."
msgstr "Es kann kein QR-Code angezeigt werden."
#: finance/templates/admin/confirmed_statement.html:99
msgid "Showing"
msgstr "Sichtbar"
#: finance/templates/admin/confirmed_statement.html:111
msgid "I did execute the listed transactions." msgid "I did execute the listed transactions."
msgstr "Ich habe die aufgeführten Überweisungen ausgeführt." msgstr "Ich habe die aufgeführten Überweisungen ausgeführt."
#: finance/templates/admin/confirmed_statement.html:68 #: finance/templates/admin/confirmed_statement.html:113
msgid "Confirm" msgid "Confirm"
msgstr "Bestätigen" msgstr "Bestätigen"

@ -15,6 +15,9 @@ from contrib.models import CommonModel
from contrib.rules import has_global_perm from contrib.rules import has_global_perm
from utils import cvt_to_decimal, RestrictedFileField from utils import cvt_to_decimal, RestrictedFileField
from schwifty import IBAN
import re
# Create your models here. # Create your models here.
class Ledger(models.Model): class Ledger(models.Model):
@ -495,6 +498,45 @@ class Transaction(models.Model):
def __str__(self): def __str__(self):
return "T#{}".format(self.pk) return "T#{}".format(self.pk)
@staticmethod
def escape_reference(reference):
umlaut_map = {
'ä': 'ae', 'ö': 'oe', 'ü': 'ue',
'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue',
'ß': 'ss'
}
pattern = re.compile('|'.join(umlaut_map.keys()))
int_reference = pattern.sub(lambda x: umlaut_map[x.group()], reference)
allowed_chars = r"[^a-z0-9 /?: .,'+-]"
clean_reference = re.sub(allowed_chars, '', int_reference, flags=re.IGNORECASE)
return clean_reference
def code(self):
if self.amount == 0:
return ""
iban = IBAN(self.member.iban, allow_invalid=True)
if not iban.is_valid:
return ""
bic = iban.bic
reference = self.escape_reference(self.reference)
# also escaping receiver as umlaute are also not allowed here
receiver = self.escape_reference(f"{self.member.prename} {self.member.lastname}")
return f"""BCD
001
1
SCT
{bic}
{receiver}
{iban}
EUR{self.amount}
{reference}"""
class Meta: class Meta:
verbose_name = _('Transaction') verbose_name = _('Transaction')
verbose_name_plural = _('Transactions') verbose_name_plural = _('Transactions')

@ -7,6 +7,8 @@
<script src="{% static 'admin/js/cancel.js' %}" async></script> <script src="{% static 'admin/js/cancel.js' %}" async></script>
<script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script> <script type="text/javascript" src="{% static "admin/js/vendor/jquery/jquery.js" %}"></script>
<script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script> <script type="text/javascript" src="{% static "admin/js/jquery.init.js" %}"></script>
<script type="text/javascript" src="{% static "js/qrcode.js" %}"></script>
{% endblock %} {% endblock %}
{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view {% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} admin-view
@ -36,6 +38,7 @@
<td>{% trans "Amount" %}</td> <td>{% trans "Amount" %}</td>
<td>{% trans "Reference" %}</td> <td>{% trans "Reference" %}</td>
<td>{% trans "Ledger" %}</td> <td>{% trans "Ledger" %}</td>
<td>{% trans "QR Code" %}</td>
</th> </th>
{% for transaction in statement.transaction_set.all %} {% for transaction in statement.transaction_set.all %}
<tr> <tr>
@ -54,11 +57,53 @@
<td> <td>
{{ transaction.ledger }} {{ transaction.ledger }}
</td> </td>
<td>
<a href="#" data-text="{{ transaction.code }}">{% trans "Show" %}</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</p> </p>
<div id="qr_code" style="display: none;"></div>
<script type="text/javascript">
const links = document.querySelectorAll('td a');
const imageContainer = document.getElementById('qr_code');
// Add click event listeners to all links
links.forEach(link => {
link.addEventListener('click', function (event) {
event.preventDefault(); // Prevent default link behavior
const imageText = this.getAttribute('data-text'); // Get the image path from the data attribute
imageContainer.innerHTML = '';
// Update the image element
if(imageText == "") {
imageContainer.innerHTML = '{% trans "No QR code can be displayed." %}';
} else {
var qrcode = new QRCode(imageContainer, {
text: imageText,
width: 128,
height: 128,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
});
}
imageContainer.style.display = 'block'; // Show the image if hidden
links.forEach(link => {link.text = '{% trans "Show" %}'});
link.text = '{% trans "Showing" %}';
});
});
</script>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<p> <p>

@ -26,7 +26,7 @@ from django.db.models import TextField, ManyToManyField, ForeignKey, Count,\
Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef Sum, Case, Q, F, When, Value, IntegerField, Subquery, OuterRef
from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput from django.forms import Textarea, RadioSelect, TypedChoiceField, CheckboxInput
from django.shortcuts import render from django.shortcuts import render
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied, ValidationError
from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf from .pdf import render_tex, fill_pdf_form, merge_pdfs, serve_pdf
from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin from contrib.admin import CommonAdminInlineMixin, CommonAdminMixin
@ -43,6 +43,8 @@ from finance.models import Statement, BillOnExcursionProxy
from mailer.mailutils import send as send_mail, get_echo_link from mailer.mailutils import send as send_mail, get_echo_link
from django.conf import settings from django.conf import settings
from utils import get_member, RestrictedFileField from utils import get_member, RestrictedFileField
from schwifty import IBAN
#from easy_select2 import apply_select2 #from easy_select2 import apply_select2
@ -161,6 +163,20 @@ class RegistrationFilter(admin.SimpleListFilter):
'display': title 'display': title
} }
class MemberAdminForm(forms.ModelForm):
class Meta:
model = Member
fields = '__all__'
# check iban validity using schwifty package
def clean_iban(self):
iban_str = self.cleaned_data.get('iban')
if len(iban_str) > 0:
iban = IBAN(iban_str, allow_invalid=True)
if not iban.is_valid:
raise ValidationError(_("The entered IBAN is not valid."))
return iban_str
# Register your models here. # Register your models here.
class MemberAdmin(CommonAdminMixin, admin.ModelAdmin): class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
@ -223,6 +239,8 @@ class MemberAdmin(CommonAdminMixin, admin.ModelAdmin):
actions = ['request_echo', 'invite_as_user_action'] actions = ['request_echo', 'invite_as_user_action']
list_per_page = 25 list_per_page = 25
form = MemberAdminForm
sensitive_fields = ['iban', 'registration_form', 'comments'] sensitive_fields = ['iban', 'registration_form', 'comments']
field_view_permissions = { field_view_permissions = {

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-27 18:18+0100\n" "POT-Creation-Date: 2024-12-28 22:56+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,198 +18,202 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: members/admin.py:127 members/models.py:430 #: members/admin.py:129 members/models.py:430
msgid "Registration complete" msgid "Registration complete"
msgstr "Anmeldung vollständig" msgstr "Anmeldung vollständig"
#: members/admin.py:133 #: members/admin.py:135
msgid "True" msgid "True"
msgstr "Ja" msgstr "Ja"
#: members/admin.py:134 #: members/admin.py:136
msgid "False" msgid "False"
msgstr "Nein" msgstr "Nein"
#: members/admin.py:135 #: members/admin.py:137
msgid "All" msgid "All"
msgstr "Alle" msgstr "Alle"
#: members/admin.py:185 members/admin.py:414 #: members/admin.py:178
msgid "The entered IBAN is not valid."
msgstr "Die eingegebene IBAN ist ungültig."
#: members/admin.py:201 members/admin.py:432
msgid "Contact information" msgid "Contact information"
msgstr "Kontaktinformationen" msgstr "Kontaktinformationen"
#: members/admin.py:190 members/admin.py:419 #: members/admin.py:206 members/admin.py:437
msgid "Skills" msgid "Skills"
msgstr "Fähigkeiten" msgstr "Fähigkeiten"
#: members/admin.py:195 members/admin.py:424 #: members/admin.py:211 members/admin.py:442
msgid "Others" msgid "Others"
msgstr "Sonstiges" msgstr "Sonstiges"
#: members/admin.py:201 members/admin.py:429 #: members/admin.py:217 members/admin.py:447
msgid "Organizational" msgid "Organizational"
msgstr "Organisatorisches" msgstr "Organisatorisches"
#: members/admin.py:282 #: members/admin.py:300
msgid "Compose new mail to selected members" msgid "Compose new mail to selected members"
msgstr "Neue Nachricht an ausgewählte Teilnehmer*innen verfassen" msgstr "Neue Nachricht an ausgewählte Teilnehmer*innen verfassen"
#: members/admin.py:288 #: members/admin.py:306
msgid "Echo required" msgid "Echo required"
msgstr "Rückmeldung erforderlich" msgstr "Rückmeldung erforderlich"
#: members/admin.py:290 #: members/admin.py:308
msgid "Successfully requested echo from selected members." msgid "Successfully requested echo from selected members."
msgstr "" msgstr ""
"Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer*innen " "Rückmeldungsaufforderung erfolgreich an ausgewählte Teilnehmer*innen "
"verschickt." "verschickt."
#: members/admin.py:291 #: members/admin.py:309
msgid "Request echo from selected members" msgid "Request echo from selected members"
msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer*innen verschicken" msgstr "Rückmeldungsaufforderung an ausgewählte Teilnehmer*innen verschicken"
#: members/admin.py:300 #: members/admin.py:318
#, python-format #, python-format
msgid "%(name)s does not have a DAV360 email address or is already registered." msgid "%(name)s does not have a DAV360 email address or is already registered."
msgstr "%(name)s hat keine DAV360 E-Mail Adresse oder ist bereits registriert." msgstr "%(name)s hat keine DAV360 E-Mail Adresse oder ist bereits registriert."
#: members/admin.py:302 #: members/admin.py:320
#, python-format #, python-format
msgid "Successfully invited %(name)s as user." msgid "Successfully invited %(name)s as user."
msgstr "Erfolgreich %(name)s aufgefordert Zugangsdaten zu wählen." msgstr "Erfolgreich %(name)s aufgefordert Zugangsdaten zu wählen."
#: members/admin.py:304 #: members/admin.py:322
msgid "Successfully invited selected members to join as users." msgid "Successfully invited selected members to join as users."
msgstr "" msgstr ""
"Erfolgreich ausgewählte Teilnehmer*innen aufgefordert Zugangsdaten zu wählen." "Erfolgreich ausgewählte Teilnehmer*innen aufgefordert Zugangsdaten zu wählen."
#: members/admin.py:306 #: members/admin.py:324
msgid "Some members have been invited, others could not be invited." msgid "Some members have been invited, others could not be invited."
msgstr "" msgstr ""
"Manche Teilnehmer*innen wurden eingeladen, andere konnten nicht eingeladen " "Manche Teilnehmer*innen wurden eingeladen, andere konnten nicht eingeladen "
"werden." "werden."
#: members/admin.py:313 members/admin.py:330 #: members/admin.py:331 members/admin.py:348
msgid "Permission denied." msgid "Permission denied."
msgstr "Fehlende Berechtigungen." msgstr "Fehlende Berechtigungen."
#: members/admin.py:320 members/admin.py:354 #: members/admin.py:338 members/admin.py:372
#: members/templates/admin/invite_as_user.html:21 #: members/templates/admin/invite_as_user.html:21
msgid "Invite as user" msgid "Invite as user"
msgstr "Kompass Zugangsdaten wählen lassen" msgstr "Kompass Zugangsdaten wählen lassen"
#: members/admin.py:325 #: members/admin.py:343
msgid "Invite selected members to join Kompass as users." msgid "Invite selected members to join Kompass as users."
msgstr "Ausgewählte Teilnehmer*innen Kompass Zugangsdaten wählen lassen." msgstr "Ausgewählte Teilnehmer*innen Kompass Zugangsdaten wählen lassen."
#: members/admin.py:336 #: members/admin.py:354
msgid "Member not found." msgid "Member not found."
msgstr "Teilnehmer*in nicht gefunden." msgstr "Teilnehmer*in nicht gefunden."
#: members/admin.py:340 #: members/admin.py:358
#, python-format #, python-format
msgid "%(name)s already has login data." msgid "%(name)s already has login data."
msgstr "%(name)s hat schon Zugangsdaten." msgstr "%(name)s hat schon Zugangsdaten."
#: members/admin.py:345 #: members/admin.py:363
#, python-format #, python-format
msgid "The configured email address for %(name)s is not an internal one." msgid "The configured email address for %(name)s is not an internal one."
msgstr "Die für %(name)s eingestellte E-Mail Adresse ist keine DAV360 Adresse." msgstr "Die für %(name)s eingestellte E-Mail Adresse ist keine DAV360 Adresse."
#: members/admin.py:359 #: members/admin.py:377
#, python-format #, python-format
msgid "%(name)s already has a pending invitation as user." msgid "%(name)s already has a pending invitation as user."
msgstr "" msgstr ""
"%(name)s hat bereits eine ausstehende Aufforderung Zugangsdaten zu wählen." "%(name)s hat bereits eine ausstehende Aufforderung Zugangsdaten zu wählen."
#: members/admin.py:377 #: members/admin.py:395
msgid "activity" msgid "activity"
msgstr "Aktivität" msgstr "Aktivität"
#: members/admin.py:387 members/models.py:56 members/models.py:1584 #: members/admin.py:405 members/models.py:56 members/models.py:1584
msgid "Name" msgid "Name"
msgstr "Name" msgstr "Name"
#: members/admin.py:478 #: members/admin.py:496
msgid "Successfully requested mail confirmation from selected registrations." msgid "Successfully requested mail confirmation from selected registrations."
msgstr "Aufforderung zur Bestätigung der Email Adresse versendet." msgstr "Aufforderung zur Bestätigung der Email Adresse versendet."
#: members/admin.py:479 #: members/admin.py:497
msgid "Request mail confirmation from selected registrations" msgid "Request mail confirmation from selected registrations"
msgstr "Aufforderung zur Bestätigung der Email Adresse versenden" msgstr "Aufforderung zur Bestätigung der Email Adresse versenden"
#: members/admin.py:486 members/admin.py:551 #: members/admin.py:504 members/admin.py:569
#, python-format #, python-format
msgid "Successfully confirmed %(name)s." msgid "Successfully confirmed %(name)s."
msgstr "Registrierung von %(name)s erfolgreich bestätigt." msgstr "Registrierung von %(name)s erfolgreich bestätigt."
#: members/admin.py:490 members/admin.py:554 #: members/admin.py:508 members/admin.py:572
#, python-format #, python-format
msgid "Can't confirm. %(name)s has unconfirmed email addresses." msgid "Can't confirm. %(name)s has unconfirmed email addresses."
msgstr "Bestätigung nicht möglich. %(name)s hat unbestätigte Emailadressen." msgstr "Bestätigung nicht möglich. %(name)s hat unbestätigte Emailadressen."
#: members/admin.py:495 #: members/admin.py:513
msgid "Successfully confirmed multiple registrations." msgid "Successfully confirmed multiple registrations."
msgstr "Erfolgreich mehrere Registrierungen bestätigt." msgstr "Erfolgreich mehrere Registrierungen bestätigt."
#: members/admin.py:497 #: members/admin.py:515
msgid "" msgid ""
"Failed to confirm some registrations because of unconfirmed email addresses." "Failed to confirm some registrations because of unconfirmed email addresses."
msgstr "" msgstr ""
"Einige Bestätigungen fehlgeschlagen, weil Emailadressen noch nicht bestätigt " "Einige Bestätigungen fehlgeschlagen, weil Emailadressen noch nicht bestätigt "
"sind." "sind."
#: members/admin.py:498 #: members/admin.py:516
msgid "Confirm selected registrations" msgid "Confirm selected registrations"
msgstr "Ausgewählte Registrierungen bestätigen" msgstr "Ausgewählte Registrierungen bestätigen"
#: members/admin.py:521 #: members/admin.py:539
msgid "Demote selected registrations to waiters." msgid "Demote selected registrations to waiters."
msgstr "Ausgewählte Registrierungen zurück auf die Warteliste setzen." msgstr "Ausgewählte Registrierungen zurück auf die Warteliste setzen."
#: members/admin.py:537 #: members/admin.py:555
msgid "Demote member to waiter" msgid "Demote member to waiter"
msgstr "Ausgewählte Registrierung zurück auf die Warteliste setzen." msgstr "Ausgewählte Registrierung zurück auf die Warteliste setzen."
#: members/admin.py:546 #: members/admin.py:564
#, python-format #, python-format
msgid "Successfully demoted %(name)s to waiter." msgid "Successfully demoted %(name)s to waiter."
msgstr "%(name)s zurück auf die Warteliste gesetzt." msgstr "%(name)s zurück auf die Warteliste gesetzt."
#: members/admin.py:561 members/models.py:437 members/models.py:840 #: members/admin.py:579 members/models.py:437 members/models.py:840
#: members/models.py:1329 #: members/models.py:1329
msgid "Group" msgid "Group"
msgstr "Gruppe" msgstr "Gruppe"
#: members/admin.py:566 #: members/admin.py:584
msgid "Invitation text" msgid "Invitation text"
msgstr "Einladungstext" msgstr "Einladungstext"
#: members/admin.py:582 #: members/admin.py:600
msgid "Pending group invitation for group" msgid "Pending group invitation for group"
msgstr "Ausstehende Gruppeneinladung für Gruppe" msgstr "Ausstehende Gruppeneinladung für Gruppe"
#: members/admin.py:615 #: members/admin.py:633
#, python-format #, python-format
msgid "Successfully asked %(name)s to confirm their waiting status." msgid "Successfully asked %(name)s to confirm their waiting status."
msgstr "Erfolgreich %(name)s aufgefordert den Wartelistenplatz zu bestätigen." msgstr "Erfolgreich %(name)s aufgefordert den Wartelistenplatz zu bestätigen."
#: members/admin.py:616 #: members/admin.py:634
msgid "Ask selected waiters to confirm their waiting status" msgid "Ask selected waiters to confirm their waiting status"
msgstr "Wartende auffordern den Wartelistenplatz zu bestätigen" msgstr "Wartende auffordern den Wartelistenplatz zu bestätigen"
#: members/admin.py:651 members/admin.py:681 #: members/admin.py:669
msgid "Offer waiter a place in a group." msgid "Offer waiter a place in a group."
msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten." msgstr "Personen auf der Warteliste einen Gruppenplatz anbieten."
#: members/admin.py:660 members/admin.py:698 members/admin.py:726 #: members/admin.py:686 members/admin.py:714
msgid "" msgid ""
"An error occurred while trying to invite said members. Please try again." "An error occurred while trying to invite said members. Please try again."
msgstr "" msgstr ""
"Beim Einladen dieser Personen ist ein Fehler aufgetreten. Bitte versuche es " "Beim Einladen dieser Personen ist ein Fehler aufgetreten. Bitte versuche es "
"nochmal. " "nochmal. "
#: members/admin.py:664 members/admin.py:703 #: members/admin.py:691
msgid "" msgid ""
"The selected group does not have a contact email. Please first set a contact " "The selected group does not have a contact email. Please first set a contact "
"email and then try again." "email and then try again."
@ -217,39 +221,39 @@ msgstr ""
"Die ausgewählte Gruppe hat keine Kontakt E-Mail Adresse. Bitte stelle eine " "Die ausgewählte Gruppe hat keine Kontakt E-Mail Adresse. Bitte stelle eine "
"Kontakt E-Mail Adresse ein und versuche es erneut." "Kontakt E-Mail Adresse ein und versuche es erneut."
#: members/admin.py:670 members/admin.py:731 #: members/admin.py:694 members/admin.py:728
msgid "Select group for invitation"
msgstr "Wähle Gruppe für Einladung aus"
#: members/admin.py:719
#, python-format #, python-format
msgid "Successfully invited %(name)s to %(group)s." msgid "Successfully invited %(name)s to %(group)s."
msgstr "Erfolgreich %(name)s zu Gruppe %(group)s eingeladen." msgstr "Erfolgreich %(name)s zu Gruppe %(group)s eingeladen."
#: members/admin.py:674 members/admin.py:706 members/admin.py:740 #: members/admin.py:748 members/models.py:72
msgid "Select group for invitation"
msgstr "Wähle Gruppe für Einladung aus"
#: members/admin.py:760 members/models.py:72
msgid "name" msgid "name"
msgstr "Name" msgstr "Name"
#: members/admin.py:761 #: members/admin.py:749
msgid "" msgid ""
"The group name may only consist of letters, numerals, _, -, :, * and spaces." "The group name may only consist of letters, numerals, _, -, :, * and spaces."
msgstr "" msgstr ""
"Der Gruppenname darf nur aus Buchstaben, Zahlen, _, -, :, * oder Leerzeichen " "Der Gruppenname darf nur aus Buchstaben, Zahlen, _, -, :, * oder Leerzeichen "
"bestehen." "bestehen."
#: members/admin.py:790 #: members/admin.py:778
msgid "Difficulty" msgid "Difficulty"
msgstr "Schwierigkeit" msgstr "Schwierigkeit"
#: members/admin.py:793 #: members/admin.py:781
msgid "Tour type" msgid "Tour type"
msgstr "Art der Tour" msgstr "Art der Tour"
#: members/admin.py:796 members/models.py:1060 #: members/admin.py:784 members/models.py:1060
msgid "Means of transportation" msgid "Means of transportation"
msgstr "Verkehrsmittel" msgstr "Verkehrsmittel"
#: members/admin.py:823 #: members/admin.py:811
msgid "" msgid ""
"Please list here all expenses in relation with this excursion and upload " "Please list here all expenses in relation with this excursion and upload "
"relevant bills. These have to be permanently stored for the application of " "relevant bills. These have to be permanently stored for the application of "
@ -262,7 +266,7 @@ msgstr ""
"einzelnen Posten wird dabei auf der LJP-Kostenübersicht angezeigt (sinnvoll " "einzelnen Posten wird dabei auf der LJP-Kostenübersicht angezeigt (sinnvoll "
"wären z.B. Anreise, Verpflegung, Material etc.)." "wären z.B. Anreise, Verpflegung, Material etc.)."
#: members/admin.py:841 #: members/admin.py:829
msgid "" msgid ""
"Here you can work on a seminar report for applying for financial " "Here you can work on a seminar report for applying for financial "
"contributions from Landesjugendplan (LJP). More information on creating a " "contributions from Landesjugendplan (LJP). More information on creating a "
@ -275,7 +279,7 @@ msgstr ""
"wahlweise nur TN-Liste und Kostenübersicht kannst du anschließend " "wahlweise nur TN-Liste und Kostenübersicht kannst du anschließend "
"herunterladen." "herunterladen."
#: members/admin.py:849 #: members/admin.py:837
msgid "" msgid ""
"Please list all participants (also youth leaders) of this excursion. Here " "Please list all participants (also youth leaders) of this excursion. Here "
"you can still make changes just before departure and hence generate the " "you can still make changes just before departure and hence generate the "
@ -286,34 +290,34 @@ msgstr ""
"jederzeit die aktuelle Teilnehmer*innenliste für die Krisenintervention " "jederzeit die aktuelle Teilnehmer*innenliste für die Krisenintervention "
"generieren." "generieren."
#: members/admin.py:895 #: members/admin.py:883
#, python-format #, python-format
msgid "You are not allowed to view all members on note list %(name)s." msgid "You are not allowed to view all members on note list %(name)s."
msgstr "" msgstr ""
"Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Notizliste " "Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Notizliste "
"%(name)s anzusehen." "%(name)s anzusehen."
#: members/admin.py:905 #: members/admin.py:893
msgid "Generate PDF summary" msgid "Generate PDF summary"
msgstr "Übersicht erstellen" msgstr "Übersicht erstellen"
#: members/admin.py:909 #: members/admin.py:897
msgid "Full report" msgid "Full report"
msgstr "Vollständiger Seminarbericht" msgstr "Vollständiger Seminarbericht"
#: members/admin.py:910 #: members/admin.py:898
msgid "Costs and participants only" msgid "Costs and participants only"
msgstr "Nur Kosten und Teilnehmende" msgstr "Nur Kosten und Teilnehmende"
#: members/admin.py:911 #: members/admin.py:899
msgid "Mode" msgid "Mode"
msgstr "Modus" msgstr "Modus"
#: members/admin.py:912 #: members/admin.py:900
msgid "Prepend V32" msgid "Prepend V32"
msgstr "V32 Formblatt einfügen" msgstr "V32 Formblatt einfügen"
#: members/admin.py:928 #: members/admin.py:916
msgid "" msgid ""
"General information on your excursion. These are partly relevant for the " "General information on your excursion. These are partly relevant for the "
"amount of financial compensation (means of transport, travel distance, etc.)." "amount of financial compensation (means of transport, travel distance, etc.)."
@ -322,48 +326,48 @@ msgstr ""
"teilweise relevant für die Zuschüsse aus dem Jugendetat (Verkehrsmittel, " "teilweise relevant für die Zuschüsse aus dem Jugendetat (Verkehrsmittel, "
"Fahrstrecke in km)." "Fahrstrecke in km)."
#: members/admin.py:958 #: members/admin.py:946
#, python-format #, python-format
msgid "You are not allowed to view all members on excursion %(name)s." msgid "You are not allowed to view all members on excursion %(name)s."
msgstr "" msgstr ""
"Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Ausfahrt " "Du hast nicht die nötigen Rechte um alle Teilnehmer*innen der Ausfahrt "
"%(name)s anzusehen." "%(name)s anzusehen."
#: members/admin.py:966 #: members/admin.py:954
msgid "Generate crisis intervention list" msgid "Generate crisis intervention list"
msgstr "Kriseninterventionsliste erstellen" msgstr "Kriseninterventionsliste erstellen"
#: members/admin.py:974 #: members/admin.py:962
msgid "Generate overview" msgid "Generate overview"
msgstr "Hinweise für Jugendleiter erstellen" msgstr "Hinweise für Jugendleiter erstellen"
#: members/admin.py:978 members/admin.py:1010 #: members/admin.py:966 members/admin.py:998
#: members/templates/admin/generate_seminar_report.html:21 #: members/templates/admin/generate_seminar_report.html:21
msgid "Generate seminar report" msgid "Generate seminar report"
msgstr "Landesjugendplan Antrag erstellen" msgstr "Landesjugendplan Antrag erstellen"
#: members/admin.py:991 #: members/admin.py:979
msgid "Please select a mode." msgid "Please select a mode."
msgstr "Bitte wähle einen Modus aus." msgstr "Bitte wähle einen Modus aus."
#: members/admin.py:996 #: members/admin.py:984
msgid "" msgid ""
"Full mode is only available, if the seminar report section is filled out." "Full mode is only available, if the seminar report section is filled out."
msgstr "" msgstr ""
"Der vollständiger Modus ist nur verfügbar, wenn der Seminarbericht " "Der vollständiger Modus ist nur verfügbar, wenn der Seminarbericht "
"ausgefüllt ist. " "ausgefüllt ist. "
#: members/admin.py:1022 #: members/admin.py:1010
msgid "Generate SJR application" msgid "Generate SJR application"
msgstr "SJR Antrag erstellen" msgstr "SJR Antrag erstellen"
#: members/admin.py:1026 #: members/admin.py:1014
msgid "No statement found. Please add a statement and then retry." msgid "No statement found. Please add a statement and then retry."
msgstr "" msgstr ""
"Keine Abrechnung angelegt. Bitte lege eine Abrechnung and und versuche es " "Keine Abrechnung angelegt. Bitte lege eine Abrechnung and und versuche es "
"erneut." "erneut."
#: members/admin.py:1030 #: members/admin.py:1018
msgid "" msgid ""
"Successfully submited statement. The finance department will notify you as " "Successfully submited statement. The finance department will notify you as "
"soon as possible." "soon as possible."
@ -371,7 +375,7 @@ msgstr ""
"Abrechnung erfolgreich eingericht. Die Finanzabteilung wird sich bei dir so " "Abrechnung erfolgreich eingericht. Die Finanzabteilung wird sich bei dir so "
"schnell wie möglich melden." "schnell wie möglich melden."
#: members/admin.py:1033 #: members/admin.py:1021
#: members/templates/admin/freizeit_finance_overview.html:21 #: members/templates/admin/freizeit_finance_overview.html:21
msgid "Finance overview" msgid "Finance overview"
msgstr "Kostenübersicht" msgstr "Kostenübersicht"
@ -1316,8 +1320,8 @@ msgid ""
msgstr "" msgstr ""
"Der folgende Text wird in der Einladungsmail verschickt. Die Platzhalter " "Der folgende Text wird in der Einladungsmail verschickt. Die Platzhalter "
"{name}, {link} und {invitation_reject_link} werden beim Senden automatisch " "{name}, {link} und {invitation_reject_link} werden beim Senden automatisch "
"durch personalisierte Daten ersetzt. Bitte passe den Text falls nötig an " "durch personalisierte Daten ersetzt. Bitte passe den Text falls nötig an und "
"und schicke die Einladung anschließend ab." "schicke die Einladung anschließend ab."
#: members/templates/admin/invite_for_group_text.html:62 #: members/templates/admin/invite_for_group_text.html:62
msgid "Send" msgid "Send"

@ -0,0 +1,614 @@
/**
* @fileoverview
* - Using the 'QRCode for Javascript library'
* - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
* - this library has no dependencies.
*
* @author davidshimjs
* @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
* @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
*/
var QRCode;
(function () {
//---------------------------------------------------------------------
// QRCode for JavaScript
//
// Copyright (c) 2009 Kazuhiko Arase
//
// URL: http://www.d-project.com/
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license.php
//
// The word "QR Code" is registered trademark of
// DENSO WAVE INCORPORATED
// http://www.denso-wave.com/qrcode/faqpatent-e.html
//
//---------------------------------------------------------------------
function QR8bitByte(data) {
this.mode = QRMode.MODE_8BIT_BYTE;
this.data = data;
this.parsedData = [];
// Added to support UTF-8 Characters
for (var i = 0, l = this.data.length; i < l; i++) {
var byteArray = [];
var code = this.data.charCodeAt(i);
if (code > 0x10000) {
byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[3] = 0x80 | (code & 0x3F);
} else if (code > 0x800) {
byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
byteArray[2] = 0x80 | (code & 0x3F);
} else if (code > 0x80) {
byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
byteArray[1] = 0x80 | (code & 0x3F);
} else {
byteArray[0] = code;
}
this.parsedData.push(byteArray);
}
this.parsedData = Array.prototype.concat.apply([], this.parsedData);
if (this.parsedData.length != this.data.length) {
this.parsedData.unshift(191);
this.parsedData.unshift(187);
this.parsedData.unshift(239);
}
}
QR8bitByte.prototype = {
getLength: function (buffer) {
return this.parsedData.length;
},
write: function (buffer) {
for (var i = 0, l = this.parsedData.length; i < l; i++) {
buffer.put(this.parsedData[i], 8);
}
}
};
function QRCodeModel(typeNumber, errorCorrectLevel) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
this.modules[r][6]=(r%2==0);}
for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
+buffer.getLengthInBits()
+">"
+totalDataCount*8
+")");}
if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
buffer.put(QRCodeModel.PAD1,8);}
return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
if(r==0&&c==0){continue;}
if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
while(n>=256){n-=255;}
return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
function _isSupportCanvas() {
return typeof CanvasRenderingContext2D != "undefined";
}
// android 2.x doesn't support Data-URI spec
function _getAndroid() {
var android = false;
var sAgent = navigator.userAgent;
if (/android/i.test(sAgent)) { // android
android = true;
var aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i);
if (aMat && aMat[1]) {
android = parseFloat(aMat[1]);
}
}
return android;
}
var svgDrawer = (function() {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
this.clear();
function makeSVG(tag, attrs) {
var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (var k in attrs)
if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]);
return el;
}
var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight});
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
_el.appendChild(svg);
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}));
svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"}));
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
if (oQRCode.isDark(row, col)) {
var child = makeSVG("use", {"x": String(col), "y": String(row)});
child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
svg.appendChild(child);
}
}
}
};
Drawing.prototype.clear = function () {
while (this._el.hasChildNodes())
this._el.removeChild(this._el.lastChild);
};
return Drawing;
})();
var useSVG = document.documentElement.tagName.toLowerCase() === "svg";
// Drawing in DOM by using Table tag
var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () {
var Drawing = function (el, htOption) {
this._el = el;
this._htOption = htOption;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _htOption = this._htOption;
var _el = this._el;
var nCount = oQRCode.getModuleCount();
var nWidth = Math.floor(_htOption.width / nCount);
var nHeight = Math.floor(_htOption.height / nCount);
var aHTML = ['<table style="border:0;border-collapse:collapse;">'];
for (var row = 0; row < nCount; row++) {
aHTML.push('<tr>');
for (var col = 0; col < nCount; col++) {
aHTML.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:' + nWidth + 'px;height:' + nHeight + 'px;background-color:' + (oQRCode.isDark(row, col) ? _htOption.colorDark : _htOption.colorLight) + ';"></td>');
}
aHTML.push('</tr>');
}
aHTML.push('</table>');
_el.innerHTML = aHTML.join('');
// Fix the margin values as real size.
var elTable = _el.childNodes[0];
var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2;
var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2;
if (nLeftMarginTable > 0 && nTopMarginTable > 0) {
elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px";
}
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._el.innerHTML = '';
};
return Drawing;
})() : (function () { // Drawing in Canvas
function _onMakeImage() {
this._elImage.src = this._elCanvas.toDataURL("image/png");
this._elImage.style.display = "block";
this._elCanvas.style.display = "none";
}
// Android 2.1 bug workaround
// http://code.google.com/p/android/issues/detail?id=5141
if (this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
var drawImage = CanvasRenderingContext2D.prototype.drawImage;
CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) {
if (("nodeName" in image) && /img/i.test(image.nodeName)) {
for (var i = arguments.length - 1; i >= 1; i--) {
arguments[i] = arguments[i] * factor;
}
} else if (typeof dw == "undefined") {
arguments[1] *= factor;
arguments[2] *= factor;
arguments[3] *= factor;
arguments[4] *= factor;
}
drawImage.apply(this, arguments);
};
}
/**
* Check whether the user's browser supports Data URI or not
*
* @private
* @param {Function} fSuccess Occurs if it supports Data URI
* @param {Function} fFail Occurs if it doesn't support Data URI
*/
function _safeSetDataURI(fSuccess, fFail) {
var self = this;
self._fFail = fFail;
self._fSuccess = fSuccess;
// Check it just once
if (self._bSupportDataURI === null) {
var el = document.createElement("img");
var fOnError = function() {
self._bSupportDataURI = false;
if (self._fFail) {
self._fFail.call(self);
}
};
var fOnSuccess = function() {
self._bSupportDataURI = true;
if (self._fSuccess) {
self._fSuccess.call(self);
}
};
el.onabort = fOnError;
el.onerror = fOnError;
el.onload = fOnSuccess;
el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data.
return;
} else if (self._bSupportDataURI === true && self._fSuccess) {
self._fSuccess.call(self);
} else if (self._bSupportDataURI === false && self._fFail) {
self._fFail.call(self);
}
};
/**
* Drawing QRCode by using canvas
*
* @constructor
* @param {HTMLElement} el
* @param {Object} htOption QRCode Options
*/
var Drawing = function (el, htOption) {
this._bIsPainted = false;
this._android = _getAndroid();
this._htOption = htOption;
this._elCanvas = document.createElement("canvas");
this._elCanvas.width = htOption.width;
this._elCanvas.height = htOption.height;
el.appendChild(this._elCanvas);
this._el = el;
this._oContext = this._elCanvas.getContext("2d");
this._bIsPainted = false;
this._elImage = document.createElement("img");
this._elImage.alt = "Scan me!";
this._elImage.style.display = "none";
this._el.appendChild(this._elImage);
this._bSupportDataURI = null;
};
/**
* Draw the QRCode
*
* @param {QRCode} oQRCode
*/
Drawing.prototype.draw = function (oQRCode) {
var _elImage = this._elImage;
var _oContext = this._oContext;
var _htOption = this._htOption;
var nCount = oQRCode.getModuleCount();
var nWidth = _htOption.width / nCount;
var nHeight = _htOption.height / nCount;
var nRoundedWidth = Math.round(nWidth);
var nRoundedHeight = Math.round(nHeight);
_elImage.style.display = "none";
this.clear();
for (var row = 0; row < nCount; row++) {
for (var col = 0; col < nCount; col++) {
var bIsDark = oQRCode.isDark(row, col);
var nLeft = col * nWidth;
var nTop = row * nHeight;
_oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.lineWidth = 1;
_oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight;
_oContext.fillRect(nLeft, nTop, nWidth, nHeight);
// 안티 앨리어싱 방지 처리
_oContext.strokeRect(
Math.floor(nLeft) + 0.5,
Math.floor(nTop) + 0.5,
nRoundedWidth,
nRoundedHeight
);
_oContext.strokeRect(
Math.ceil(nLeft) - 0.5,
Math.ceil(nTop) - 0.5,
nRoundedWidth,
nRoundedHeight
);
}
}
this._bIsPainted = true;
};
/**
* Make the image from Canvas if the browser supports Data URI.
*/
Drawing.prototype.makeImage = function () {
if (this._bIsPainted) {
_safeSetDataURI.call(this, _onMakeImage);
}
};
/**
* Return whether the QRCode is painted or not
*
* @return {Boolean}
*/
Drawing.prototype.isPainted = function () {
return this._bIsPainted;
};
/**
* Clear the QRCode
*/
Drawing.prototype.clear = function () {
this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height);
this._bIsPainted = false;
};
/**
* @private
* @param {Number} nNumber
*/
Drawing.prototype.round = function (nNumber) {
if (!nNumber) {
return nNumber;
}
return Math.floor(nNumber * 1000) / 1000;
};
return Drawing;
})();
/**
* Get the type by string length
*
* @private
* @param {String} sText
* @param {Number} nCorrectLevel
* @return {Number} type
*/
function _getTypeNumber(sText, nCorrectLevel) {
var nType = 1;
var length = _getUTF8Length(sText);
for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
var nLimit = 0;
switch (nCorrectLevel) {
case QRErrorCorrectLevel.L :
nLimit = QRCodeLimitLength[i][0];
break;
case QRErrorCorrectLevel.M :
nLimit = QRCodeLimitLength[i][1];
break;
case QRErrorCorrectLevel.Q :
nLimit = QRCodeLimitLength[i][2];
break;
case QRErrorCorrectLevel.H :
nLimit = QRCodeLimitLength[i][3];
break;
}
if (length <= nLimit) {
break;
} else {
nType++;
}
}
if (nType > QRCodeLimitLength.length) {
throw new Error("Too long data");
}
return nType;
}
function _getUTF8Length(sText) {
var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
return replacedText.length + (replacedText.length != sText ? 3 : 0);
}
/**
* @class QRCode
* @constructor
* @example
* new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
*
* @example
* var oQRCode = new QRCode("test", {
* text : "http://naver.com",
* width : 128,
* height : 128
* });
*
* oQRCode.clear(); // Clear the QRCode.
* oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
*
* @param {HTMLElement|String} el target element or 'id' attribute of element.
* @param {Object|String} vOption
* @param {String} vOption.text QRCode link data
* @param {Number} [vOption.width=256]
* @param {Number} [vOption.height=256]
* @param {String} [vOption.colorDark="#000000"]
* @param {String} [vOption.colorLight="#ffffff"]
* @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
*/
QRCode = function (el, vOption) {
this._htOption = {
width : 256,
height : 256,
typeNumber : 4,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRErrorCorrectLevel.H
};
if (typeof vOption === 'string') {
vOption = {
text : vOption
};
}
// Overwrites options
if (vOption) {
for (var i in vOption) {
this._htOption[i] = vOption[i];
}
}
if (typeof el == "string") {
el = document.getElementById(el);
}
if (this._htOption.useSVG) {
Drawing = svgDrawer;
}
this._android = _getAndroid();
this._el = el;
this._oQRCode = null;
this._oDrawing = new Drawing(this._el, this._htOption);
if (this._htOption.text) {
this.makeCode(this._htOption.text);
}
};
/**
* Make the QRCode
*
* @param {String} sText link data
*/
QRCode.prototype.makeCode = function (sText) {
this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel);
this._oQRCode.addData(sText);
this._oQRCode.make();
this._el.title = sText;
this._oDrawing.draw(this._oQRCode);
this.makeImage();
};
/**
* Make the Image from Canvas element
* - It occurs automatically
* - Android below 3 doesn't support Data-URI spec.
*
* @private
*/
QRCode.prototype.makeImage = function () {
if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) {
this._oDrawing.makeImage();
}
};
/**
* Clear the QRCode
*/
QRCode.prototype.clear = function () {
this._oDrawing.clear();
};
/**
* @name QRCode.CorrectLevel
*/
QRCode.CorrectLevel = QRErrorCorrectLevel;
})();

@ -48,6 +48,7 @@ pytz==2021.3
redis==4.1.0 redis==4.1.0
requests==2.32.3 requests==2.32.3
rules==3.3 rules==3.3
schwifty==2024.11.0
six==1.16.0 six==1.16.0
snowballstemmer==2.2.0 snowballstemmer==2.2.0
Sphinx==7.4.7 Sphinx==7.4.7

Loading…
Cancel
Save