From 5bb648a647c31acd9c94a0c667c49735c5738533 Mon Sep 17 00:00:00 2001 From: Daniel Ursache Dogariu Date: Thu, 11 Jan 2024 14:32:49 +0200 Subject: [PATCH] Transcribe the donor for to the new backend (#138) * Improve form_url constraints * Fix the user verification * Code format * Show the donation success page * noqa * noqa... again * noqa specific warning * Revert twopercent template and js * Begin upgrading the PDF generation * First beta for the PDF generator on Python3 * Code format * Tweaks & fixes - add a slug to NGOs - change fields to the correct form * Update comment --------- Co-authored-by: Tudor Amariei --- backend/donations/forms.py | 2 +- ...ge_ngo_logo_alter_ngo_form_url_and_more.py | 46 ++ ...05_rename_personal_identifier_donor_cnp.py | 17 + .../migrations/0006_donor_pdf_file.py | 23 + ...orm_url_unique_ngo_custom_form_and_more.py | 55 +++ backend/donations/models.py | 107 ++++- backend/donations/pdf.py | 294 ++++++++++++ backend/donations/views/account_management.py | 3 + backend/donations/views/my_account.py | 2 +- backend/donations/views/ngo.py | 345 ++++++++++++-- backend/redirectioneaza/settings.py | 3 + backend/static_extras/js/twopercent.js | 433 ++++++++---------- backend/templates/v1/admin2/accounts.html | 4 +- backend/templates/v1/admin2/ngos.html | 2 +- backend/templates/v1/all-ngos.html | 2 +- .../templates/v1/components/be-a-hero.html | 2 +- .../v1/components/ngo-details-form.html | 8 +- .../templates/v1/components/ngo-header.html | 4 +- .../twopercent-form/twopercent_form.html | 2 +- .../twopercent-form/twopercent_form_text.txt | 2 +- backend/templates/v1/index.html | 2 +- backend/templates/v1/ngo/donations-view.html | 2 +- backend/templates/v1/ngo/my-account.html | 4 +- backend/templates/v1/signature.html | 2 +- backend/templates/v1/succes.html | 2 +- backend/templates/v1/twopercent.html | 50 +- 26 files changed, 1080 insertions(+), 338 deletions(-) create mode 100644 backend/donations/migrations/0004_ngo_image_ngo_logo_alter_ngo_form_url_and_more.py create mode 100644 backend/donations/migrations/0005_rename_personal_identifier_donor_cnp.py create mode 100644 backend/donations/migrations/0006_donor_pdf_file.py create mode 100644 backend/donations/migrations/0007_remove_ngo_form_url_unique_ngo_custom_form_and_more.py create mode 100644 backend/donations/pdf.py diff --git a/backend/donations/forms.py b/backend/donations/forms.py index 5d6e0c07..06232c0f 100644 --- a/backend/donations/forms.py +++ b/backend/donations/forms.py @@ -15,7 +15,7 @@ class DonorInputForm(forms.ModelForm): terms = forms.BooleanField(label=_("Terms"), required=True) ngo_id = forms.IntegerField(widget=forms.HiddenInput(), required=False) - personal_identifier = ROCNPField(label="CNP") + cnp = ROCNPField(label="CNP") class Meta: model = Donor diff --git a/backend/donations/migrations/0004_ngo_image_ngo_logo_alter_ngo_form_url_and_more.py b/backend/donations/migrations/0004_ngo_image_ngo_logo_alter_ngo_form_url_and_more.py new file mode 100644 index 00000000..31002929 --- /dev/null +++ b/backend/donations/migrations/0004_ngo_image_ngo_logo_alter_ngo_form_url_and_more.py @@ -0,0 +1,46 @@ +# Generated by Django 4.2.9 on 2024-01-10 04:54 + +from django.db import migrations, models +import django.db.models.functions.text +import donations.models +import functools + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0003_donor_initial_donor_personal_identifier_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="ngo", + name="image", + field=models.ImageField( + blank=True, + storage=donations.models.select_public_storage, + upload_to=functools.partial(donations.models.ngo_directory_path, *("images",), **{}), + verbose_name="image", + ), + ), + migrations.AddField( + model_name="ngo", + name="logo", + field=models.ImageField( + blank=True, + storage=donations.models.select_public_storage, + upload_to=functools.partial(donations.models.ngo_directory_path, *("logos",), **{}), + verbose_name="logo", + ), + ), + migrations.AlterField( + model_name="ngo", + name="form_url", + field=models.SlugField(max_length=100, null=True, unique=True, verbose_name="form url"), + ), + migrations.AddConstraint( + model_name="ngo", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("form_url"), name="form_url_unique" + ), + ), + ] diff --git a/backend/donations/migrations/0005_rename_personal_identifier_donor_cnp.py b/backend/donations/migrations/0005_rename_personal_identifier_donor_cnp.py new file mode 100644 index 00000000..c699eb38 --- /dev/null +++ b/backend/donations/migrations/0005_rename_personal_identifier_donor_cnp.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.9 on 2024-01-10 06:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0004_ngo_image_ngo_logo_alter_ngo_form_url_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="donor", + old_name="personal_identifier", + new_name="cnp", + ), + ] diff --git a/backend/donations/migrations/0006_donor_pdf_file.py b/backend/donations/migrations/0006_donor_pdf_file.py new file mode 100644 index 00000000..3948a09e --- /dev/null +++ b/backend/donations/migrations/0006_donor_pdf_file.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.9 on 2024-01-11 05:34 + +from django.db import migrations, models +import donations.models +import functools + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0005_rename_personal_identifier_donor_cnp"), + ] + + operations = [ + migrations.AddField( + model_name="donor", + name="pdf_file", + field=models.FileField( + blank=True, + upload_to=functools.partial(donations.models.year_directory_path, *("documents",), **{}), + verbose_name="PDF file", + ), + ), + ] diff --git a/backend/donations/migrations/0007_remove_ngo_form_url_unique_ngo_custom_form_and_more.py b/backend/donations/migrations/0007_remove_ngo_form_url_unique_ngo_custom_form_and_more.py new file mode 100644 index 00000000..d07bf96c --- /dev/null +++ b/backend/donations/migrations/0007_remove_ngo_form_url_unique_ngo_custom_form_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.9 on 2024-01-11 11:38 + +from django.db import migrations, models +import django.db.models.functions.text +import donations.models +import functools + + +class Migration(migrations.Migration): + dependencies = [ + ("donations", "0006_donor_pdf_file"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="ngo", + name="form_url_unique", + ), + migrations.AddField( + model_name="ngo", + name="custom_form", + field=models.FileField( + blank=True, + storage=donations.models.select_public_storage, + upload_to=functools.partial(donations.models.ngo_directory_path, *("forms",), **{}), + verbose_name="form with ngo data", + ), + ), + migrations.AddField( + model_name="ngo", + name="date_updated", + field=models.DateTimeField(auto_now=True, db_index=True, verbose_name="date updated"), + ), + migrations.AddField( + model_name="ngo", + name="slug", + field=models.SlugField( + default="", + max_length=100, + unique=True, + validators=[donations.models.ngo_identifier_validator], + verbose_name="slug", + ), + preserve_default=False, + ), + migrations.AlterField( + model_name="ngo", + name="form_url", + field=models.URLField(blank=True, default="", max_length=255, verbose_name="form url"), + ), + migrations.AddConstraint( + model_name="ngo", + constraint=models.UniqueConstraint(django.db.models.functions.text.Lower("slug"), name="slug__unique"), + ), + ] diff --git a/backend/donations/models.py b/backend/donations/models.py index 0ed725ef..757cbb0d 100644 --- a/backend/donations/models.py +++ b/backend/donations/models.py @@ -1,22 +1,78 @@ +import hashlib +from functools import partial + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.files.storage import storages from django.db import models +from django.db.models.functions import Lower from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django_cryptography.fields import encrypt +def select_public_storage(): + return storages["public"] + + +def ngo_directory_path(subdir, instance, filename) -> str: + ngo_code: str = hashlib.sha1(f"ngo-{instance.pk}-{settings.SECRET_KEY}".encode()).hexdigest() + + # file will be uploaded to MEDIA_ROOT/ngo--// + return "ngo-{0}-{1}/{2}/{3}".format(instance.pk, ngo_code[:10], subdir, filename) + + +def year_directory_path(subdir, instance, filename) -> str: + timestamp = timezone.now() + return "{0}/{1}/{2}/{3}".format(subdir, timestamp.date().year, instance.pk, filename) + + +def ngo_identifier_validator(value): + valid_identifier_sample: str = "asociatia-de-exemplu" + error_message = _("%(value)s is not a valid identifier. The identifier must look like %(sample)s") % { + "value": value, + "sample": valid_identifier_sample, + } + + if not value.islower(): + raise ValidationError(error_message) + + class Ngo(models.Model): # DEFAULT_NGO_LOGO = "https://storage.googleapis.com/redirectioneaza/logo_bw.png" - name = models.CharField(verbose_name=_("Name"), blank=False, null=False, max_length=100, db_index=True) - description = models.TextField( - verbose_name=_("description"), + slug = models.SlugField( + verbose_name=_("slug"), + blank=False, + null=False, + max_length=100, + db_index=True, + unique=True, + validators=[ngo_identifier_validator], ) + name = models.CharField(verbose_name=_("Name"), blank=False, null=False, max_length=100, db_index=True) + description = models.TextField(verbose_name=_("description")) + # originally: logo logo_url = models.URLField(verbose_name=_("logo url"), blank=True, null=False, default="") + logo = models.ImageField( + verbose_name=_("logo"), + blank=True, + null=False, + storage=select_public_storage, + upload_to=partial(ngo_directory_path, "logos"), + ) # originally: image_url image_url = models.URLField(verbose_name=_("image url"), blank=True, null=False, default="") + image = models.ImageField( + verbose_name=_("image"), + blank=True, + null=False, + storage=select_public_storage, + upload_to=partial(ngo_directory_path, "images"), + ) # originally: account bank_account = models.CharField(verbose_name=_("bank account"), max_length=100) @@ -70,20 +126,48 @@ class Ngo(models.Model): # originally: active is_active = models.BooleanField(verbose_name=_("is active"), db_index=True, default=True) - # url to the ngo's 2% form, that contains only the ngo's details - form_url = models.CharField( - verbose_name=_("form url"), blank=True, null=False, default="", max_length=255, unique=True + # url to the form that contains only the ngo's details + form_url = models.URLField( + verbose_name=_("form url"), + default="", + blank=True, + null=False, + max_length=255, + ) + custom_form = models.FileField( + verbose_name=_("form with ngo data"), + blank=True, + null=False, + storage=select_public_storage, + upload_to=partial(ngo_directory_path, "forms"), ) date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=timezone.now) + date_updated = models.DateTimeField(verbose_name=_("date updated"), db_index=True, auto_now=timezone.now) class Meta: verbose_name = _("NGO") verbose_name_plural = _("NGOs") + constraints = [ + models.UniqueConstraint(Lower("slug"), name="slug__unique"), + ] + def __str__(self): return f"{self.name}" + def save(self, *args, **kwargs): + # Force the form_url (which acts as an NGO identifier) to lowercase + if self.form_url: + self.form_url = self.form_url.lower().strip() + return super().save(*args, **kwargs) + + def get_full_form_url(self): + if self.form_url: + return "https://{}/{}".format(settings.APEX_DOMAIN, self.form_url) + else: + return "" + class Donor(models.Model): INCOME_CHOICES = ( @@ -97,9 +181,7 @@ class Donor(models.Model): last_name = models.CharField(verbose_name=_("last name"), blank=True, null=False, default="", max_length=100) initial = models.CharField(verbose_name=_("initials"), blank=True, null=False, default="", max_length=5) - personal_identifier = encrypt( - models.CharField(verbose_name=_("CNP"), blank=True, null=False, default="", max_length=13) - ) + cnp = encrypt(models.CharField(verbose_name=_("CNP"), blank=True, null=False, default="", max_length=13)) city = models.CharField( verbose_name=_("city"), @@ -152,6 +234,13 @@ class Donor(models.Model): filename = models.CharField(verbose_name=_("filename"), blank=True, null=False, default="", max_length=100) has_signed = models.BooleanField(verbose_name=_("has signed"), db_index=True, default=False) + pdf_file = models.FileField( + verbose_name=_("PDF file"), + blank=True, + null=False, + upload_to=partial(year_directory_path, "documents"), + ) + date_created = models.DateTimeField(verbose_name=_("date created"), db_index=True, auto_now_add=timezone.now) class Meta: diff --git a/backend/donations/pdf.py b/backend/donations/pdf.py new file mode 100644 index 00000000..84e94192 --- /dev/null +++ b/backend/donations/pdf.py @@ -0,0 +1,294 @@ +import os +from io import StringIO +import base64 +import tempfile + +from io import BytesIO +from datetime import datetime + +from pypdf import PdfFileWriter, PdfFileReader +from reportlab.pdfgen import canvas +from reportlab.lib.utils import ImageReader +from reportlab.lib.pagesizes import A4 +from reportlab.graphics import renderPDF + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from svglib.svglib import svg2rlg + + +abs_path = os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) +font_path = abs_path + "/static/font/opensans.ttf" +pdfmetrics.registerFont(TTFont("OpenSans", font_path)) + +default_font_size = 15 +form_image_path = "/static/images/formular-2021.jpg" + + +def format_ngo_account(ngo_account): + # remove white spaces from account + ngo_account = ngo_account.replace(" ", "") + + account = "" + for i, letter in enumerate(ngo_account): + account += letter + if (i + 1) % 4 == 0: + account += " " + + return account + + +def add_donor_data(c, person): + donor_block_x = 681 + + # the first name + if len(person["first_name"]) > 18: + c.setFontSize(12) + + c.drawString(75, donor_block_x, person["first_name"]) + c.setFontSize(default_font_size) + + # father's first letter + c.drawString(300, donor_block_x, person["father"]) + + # the last name + last_name = person["last_name"] + if len(last_name) > 34: + c.setFontSize(10) + + c.drawString(75, donor_block_x - 22, last_name) + + # ======================================= + # THIRD ROW + # + third_row_x = donor_block_x - 45 + + # the street + street = person["street"] + + if len(street) > 40: + c.setFontSize(8) + elif len(street) in range(36, 41): + c.setFontSize(10) + elif len(street) in range(24, 36): + c.setFontSize(11) + + c.drawString(67, third_row_x, street) + c.setFontSize(default_font_size) + + # numar + if len(person["number"]) > 5: + c.setFontSize(8) + elif len(person["number"]) > 3: + c.setFontSize(10) + + c.drawString(289, third_row_x, person["number"]) + c.setFontSize(default_font_size) + + # + # ======================================= + + # ======================================= + # FOURTH ROW + fourth_row_x = donor_block_x - 67 + + c.setFontSize(14) + # bloc + c.drawString(49, fourth_row_x, person["bl"]) + # scara + c.drawString(108, fourth_row_x, person["sc"]) + + # etaj + c.drawString(150, fourth_row_x, person["et"]) + + # apartament + c.drawString(185, fourth_row_x, person["ap"]) + + # judet + if len(person["county"]) <= 12: + c.setFontSize(12) + else: + c.setFontSize(8) + + c.drawString(255, fourth_row_x, person["county"]) + c.setFontSize(default_font_size) + # + # ======================================= + + # oras + c.drawString(69, donor_block_x - 90, person["city"]) + + c.setFontSize(16) + + # cnp + start_x = 336 + for letter in person["cnp"]: + c.drawString(start_x, donor_block_x - 10, letter) + start_x += 18.5 + + # email + start_email_x = 368 + if person["email"]: + if len(person["email"]) < 32: + c.setFontSize(12) + elif len(person["email"]) < 40: + c.setFontSize(10) + else: + c.setFontSize(8) + + c.drawString(start_email_x, third_row_x + 14, person["email"]) + + # telephone + if person["tel"]: + c.setFontSize(12) + c.drawString(start_email_x, third_row_x - 15, person["tel"]) + + c.setFontSize(default_font_size) + + +def add_ngo_data(c, ong): + start_ong_y = 449 + + # the x mark + c.drawString(219, start_ong_y, "x") + + if ong.get("two_years"): + c.drawString(325, start_ong_y - 21, "x") + + # the cif code + c.setFontSize(9) + c.drawString(250, start_ong_y - 41, ong["cif"]) + + # the name + org_name = ong["name"] + if len(org_name) > 79: + c.setFontSize(9) + elif len(org_name) > 65: + c.setFontSize(12) + + c.drawString(186, start_ong_y - 64, org_name.encode("utf-8")) + + c.setFontSize(11) + + # the bank account + account = format_ngo_account(ong["account"]) + c.drawString(110, start_ong_y - 86, account) + + if ong.get("percent"): + c.drawString(146, start_ong_y - 108, ong.get("percent")) + + +def create_pdf(person={}, ong={}): + """method used to create the pdf + + person: dict with the person's data + first_name + father + last_name + email + tel + street + number + bl + sc + et + ap + county + city + cnp + + + ong: dict with the ngo's data + name + cif + account + """ + + # packet = StringIO.StringIO() + # we could also use StringIO + packet = tempfile.TemporaryFile(mode="w+b") + + c = canvas.Canvas(packet, A4) + width, height = A4 + + # add the image as background + background = ImageReader(abs_path + form_image_path) + c.drawImage(background, 0, 0, width=width, height=height) + + c.setFont("OpenSans", default_font_size) + c.setFontSize(default_font_size) + + # the year + # this is the previous year + year = str(datetime.now().year - 1) + start_x = 305 + for letter in year: + c.drawString(start_x, 736, letter) + start_x += 18 + + # DRAW DONOR DATA + if person.get("first_name"): + add_donor_data(c, person) + + add_ngo_data(c, ong) + + c.save() + + # go to the beginning of the file + packet.seek(0) + # packet.type = "application/pdf" + + return packet + + +def add_signature(pdf, image): + pdf_string = StringIO.StringIO(pdf) + existing_pdf = PdfFileReader(pdf_string) + + packet = tempfile.TemporaryFile(mode="w+b") + + # init pdf canvas + c = canvas.Canvas(packet, A4) + + # add the image as background + # remove the header added by javascript + image = image.split(",")[1] + # make sure the string has the right padding + image = image + "=" * (-len(image) % 4) + base_image = base64.b64decode(image) + byte_image = BytesIO(base_image) + # make this a svg2rlg object + drawing = svg2rlg(byte_image) + + # we used to use width for scaling down but we move to height + # new_width = 90 + + new_height = 30 + scaled_down = new_height / drawing.height + + # we want to scale the image down and stil keep it's aspect ratio + # the image might have dimensions of 750 x 200 + drawing.scale(scaled_down, scaled_down) + + # add it to the PDF + renderPDF.draw(drawing, c, 166, 134) + + c.save() + packet.seek(0) + + new_pdf = PdfFileReader(packet) + + page = existing_pdf.getPage(0) + page.mergePage(new_pdf.getPage(0)) + + output = PdfFileWriter() + output.addPage(page) + + outputStream = tempfile.TemporaryFile(mode="w+b") + output.write(outputStream) + + outputStream.seek(0) + + packet.close() + + return outputStream diff --git a/backend/donations/views/account_management.py b/backend/donations/views/account_management.py index e47da089..9fce1617 100644 --- a/backend/donations/views/account_management.py +++ b/backend/donations/views/account_management.py @@ -281,6 +281,9 @@ def get(self, request, *args, **kwargs): user_id = kwargs["user_id"] signup_token = kwargs["signup_token"] + if verification_type not in ("p", "v"): + raise Http404 + try: user = self.user_model.objects.get(pk=user_id) except self.user_model.DoesNotExist: diff --git a/backend/donations/views/my_account.py b/backend/donations/views/my_account.py index 79c9a438..46bd0dba 100644 --- a/backend/donations/views/my_account.py +++ b/backend/donations/views/my_account.py @@ -96,7 +96,7 @@ def post(self, request, *args, **kwargs): ngo.address = post.get("ong-adresa") ngo.county = post.get("ong-judet") ngo.active_region = post.get("ong-activitate") - ngo.form_url = post.get("ong-url").lower() + ngo.slug = post.get("ong-url").lower() ngo.registration_number = post.get("ong-cif") ngo.bank_account = post.get("ong-cont") ngo.has_special_status = True if post.get("special-status") == "on" else False diff --git a/backend/donations/views/ngo.py b/backend/donations/views/ngo.py index 40e98ed6..d8705e19 100644 --- a/backend/donations/views/ngo.py +++ b/backend/donations/views/ngo.py @@ -1,24 +1,56 @@ +import re +from datetime import date +from hashlib import sha1 +from urllib.parse import urlparse + from django.conf import settings -from django.http import Http404 +from django.core.files import File +from django.http import Http404, JsonResponse from django.shortcuts import redirect, render from django.urls import reverse +from django.utils import timezone from .base import BaseHandler -from ..forms import DonorInputForm from ..models import Donor, Ngo +from ..pdf import create_pdf class DonationSucces(BaseHandler): template_name = "succes.html" - def get_context_data(self, **kwargs): + def get_context_data(self, ngo_url, **kwargs): context = super().get_context_data(**kwargs) - ngo = Ngo.objects.get(form_url=kwargs["ngo_url"].lower()) + + ngo_url = ngo_url.lower().strip() + try: + ngo = Ngo.objects.get(form_url=ngo_url) + except Ngo.DoesNotExist: + ngo = None + context["ngo"] = ngo return context - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + def get(self, request, ngo_url): + context = self.get_context_data(ngo_url) + DONATION_LIMIT = date(timezone.now().year, 5, 25) + + try: + donor = Donor.objects.get(pk=request.session.get("donor_id", 0)) + except Donor.DoesNotExist: + donor = None + context["donor"] = donor + + context["title"] = "Donație - succes" + context["limit"] = DONATION_LIMIT + + # county = self.donor.county.lower() + # context["anaf"] = ANAF_OFFICES.get(county, None) + + # for now, disable showing the ANAF office + context["anaf"] = None + + # if the user didn't provide a CNP show a message + context["has_cnp"] = request.session.get("has_cnp", False) return render(self.request, self.template_name, context) @@ -26,73 +58,290 @@ def get(self, request, *args, **kwargs): class FormSignature(BaseHandler): template_name = "signature.html" - def get_context_data(self, **kwargs): + def get_context_data(self, ngo_url, **kwargs): context = super().get_context_data(**kwargs) - ngo = Ngo.objects.get(form_url=kwargs["ngo_url"].lower()) + + ngo_url = ngo_url.lower().strip() + try: + ngo = Ngo.objects.get(form_url=ngo_url) + except Ngo.DoesNotExist: + ngo = None + context["ngo"] = ngo return context - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + def get(self, request, ngo_url): + context = self.get_context_data(ngo_url) return render(self.request, self.template_name, context) - def post(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + def post(self, request, ngo_url): + # context = self.get_context_data(ngo_url) - return redirect(reverse("ngo-twopercent-success", kwargs={"ngo_url": context["ngo_url"]})) + return redirect(reverse("ngo-twopercent-success", kwargs={"ngo_url": ngo_url})) class TwoPercentHandler(BaseHandler): - def get_context_data(self, request, **kwargs): - ngo_url: str = kwargs["ngo_url"].lower() - if not Ngo.objects.filter(form_url=ngo_url).exists(): - raise Http404("Nu exista o asociație cu acest URL") + template_name = "twopercent.html" + + def get(self, request, ngo_url): + try: + ngo = Ngo.objects.get(form_url=ngo_url) + except Ngo.DoesNotExist: + ngo = None + + # if we didn't find it or the ngo doesn't have an active page + if ngo is None or not ngo.is_active: + raise Http404 + + # if we still have a cookie from an old session, remove it + if "donor_id" in request.session: + request.session.pop("donor_id") + + if "has_cnp" in request.session: + request.session.pop("has_cnp") + # also we can use request.session.clear(), but it might delete the logged in user's session + + context = {} + context["title"] = ngo.name + # make sure the ngo shows a logo + ngo.logo_url = ngo.logo_url if ngo.logo_url else settings.DEFAULT_NGO_LOGO + context["ngo"] = ngo + context["counties"] = settings.LIST_OF_COUNTIES + context["limit"] = settings.DONATIONS_LIMIT - ngo = Ngo.objects.get(form_url=ngo_url) - form_counties = settings.FORM_COUNTIES + # the ngo website + ngo_website = ngo.website if ngo.website else None + if ngo_website: + # try and parse the the url to see if it's valid + try: + url_dict = urlparse(ngo_website) - context = {"is_authenticated": False, "ngo_url": ngo_url, "ngo": ngo, "counties": form_counties} + if not url_dict.scheme: + url_dict = url_dict._replace(scheme="http") - if request.user.is_authenticated and request.user.ngo == ngo: - context["is_authenticated"] = True - return context + # if we have a netloc, than the url is valid + # use the netloc as the website name + if url_dict.netloc: + context["ngo_website_description"] = url_dict.netloc + context["ngo_website"] = url_dict.geturl() - context["limit"] = settings.DONATIONS_LIMIT - context["can_donate"] = True + # of we don't have the netloc, when parsing the url + # urlparse might send it to path + # move that to netloc and remove the path + elif url_dict.path: + url_dict = url_dict._replace(netloc=url_dict.path) + context["ngo_website_description"] = url_dict.path - return context + url_dict = url_dict._replace(path="") + + context["ngo_website"] = url_dict.geturl() + else: + raise - def get(self, request, *args, **kwargs): - context = self.get_context_data(request, **kwargs) + except Exception: + context["ngo_website"] = None + else: + context["ngo_website"] = None - template = "twopercent.html" + now = timezone.now() + can_donate = not now.date() > settings.DONATIONS_LIMIT - if context["is_authenticated"]: - template = "ngo/ngo-details.html" + context["can_donate"] = can_donate + context["is_admin"] = request.user.is_staff # TODO: check this - return render(request, template, context) + return render(request, self.template_name, context) - def post(self, request, *args, **kwargs): - post = request.POST - context = self.get_context_data(request, **kwargs) + def post(self, request, ngo_url): + post = self.request.POST + errors = {"fields": [], "server": False} - form = DonorInputForm(post) - if not form.is_valid(): - context.update(form.cleaned_data) - context["errors"] = {"fields": list(form.errors.values())} + try: + ngo = Ngo.objects.get(form_url=ngo_url) + except Ngo.DoesNotExist: + raise Http404 + + # if we have an ajax request, just return an answer + is_ajax = post.get("ajax", False) - return render(request, "twopercent.html", context) + def get_post_value(arg, add_to_error_list=True): + value = post.get(arg) + + # if we received a value + if value: + # it should only contains alpha numeric, spaces and dash + if re.match(r"^[\w\s.\-ăîâșț]+$", value, flags=re.I | re.UNICODE) is not None: + # additional validation + if arg == "cnp" and len(value) != 13: + errors["fields"].append(arg) + return "" + + return value + + # the email has the @ so the first regex will fail + elif arg == "email": + # if we found a match + if re.match(r"[^@]+@[^@]+\.[^@]+", value) is not None: + return value + + errors["fields"].append(arg) + return "" + + else: + errors["fields"].append(arg) + + elif add_to_error_list: + errors["fields"].append(arg) + + return "" + + donor_dict = {} + + # the donor's data + donor_dict["first_name"] = get_post_value("nume").title() + donor_dict["last_name"] = get_post_value("prenume").title() + donor_dict["father"] = get_post_value("tatal").title() + donor_dict["cnp"] = get_post_value("cnp", False) + + donor_dict["email"] = get_post_value("email").lower() + donor_dict["tel"] = get_post_value("tel", False) + + donor_dict["street"] = get_post_value("strada").title() + donor_dict["number"] = get_post_value("numar", False) + + # optional data + donor_dict["bl"] = get_post_value("bloc", False) + donor_dict["sc"] = get_post_value("scara", False) + donor_dict["et"] = get_post_value("etaj", False) + donor_dict["ap"] = get_post_value("ap", False) + + donor_dict["city"] = get_post_value("localitate").title() + donor_dict["county"] = get_post_value("judet") + + # if the user wants to redirect for 2 years + two_years = post.get("two-years") == "on" + + # if the ngo accepts online forms + signature_required = False + if ngo.is_accepting_forms: + wants_to_sign = post.get("wants-to-sign", False) + if wants_to_sign == "True": + signature_required = True + + # if he would like the ngo to see the donation + donor_dict["anonymous"] = post.get("anonim") != "on" + + # what kind of income does he have: wage or other + donor_dict["income"] = post.get("income", "wage") + + # the ngo data + ngo_data = { + "name": ngo.name, + "account": ngo.bank_account.upper(), + "cif": ngo.registration_number, + "two_years": two_years, + "special_status": ngo.has_special_status, + "percent": "3,5%", + } + + if len(errors["fields"]): + return self.return_error(request, ngo, errors, is_ajax) + + ## TODO: Captcha check + # captcha_response = submit(post.get(CAPTCHA_POST_PARAM), CAPTCHA_PRIVATE_KEY, self.request.remote_addr) + + # # if the captcha is not valid return + # if not captcha_response.is_valid: + + # errors["fields"].append("codul captcha") + # self.return_error(errors) + # return + + # TODO + # the user's folder name, it's just his md5 hashed db id + # user_folder = security.hash_password('123', "md5") + user_folder = "123123" + + # a way to create unique file names + # get the local time in iso format + # run that through SHA1 hash + # output a hex string + filename = "{0}/{1}/{2}.pdf".format( + settings.USER_FORMS, str(user_folder), sha1(timezone.now().isoformat().encode("utf8")).hexdigest() + ) + + pdf = create_pdf(donor_dict, ngo_data) + + # create the donor and save it + donor = Donor( + first_name=donor_dict["first_name"], + last_name=donor_dict["last_name"], + city=donor_dict["city"], + county=donor_dict["county"], + email=donor_dict["email"], + phone=donor_dict["tel"], + is_anonymous=donor_dict["anonymous"], + two_years=two_years, + income_type=donor_dict["income"], + ## TODO: + # make a request to get geo ip data for this user + # geoip = self.get_geoip_data(), + ngo=ngo, + filename=filename, + ) + donor.save() + + donor.pdf_file.save(filename, File(pdf)) + + # close the file after it has been uploaded + pdf.close() + + # TODO: Get rid of pdf_url + donor.pdf_url = donor.pdf_file.url + donor.save() + + # set the donor id in cookie + request.session["donor_id"] = str(donor.pk) + request.session["has_cnp"] = bool(donor_dict["cnp"]) + request.session["signature_required"] = signature_required + + # TODO: Send the email + # if not signature_required: + # # send and email to the donor with a link to the PDF file + # self.send_email("twopercent-form", donor, ngo) + + url = reverse("ngo-twopercent-success", kwargs={"ngo_url": ngo_url}) + if signature_required: + url = reverse("ngo-twopercent-signature", kwargs={"ngo_url": ngo_url}) + + # if not an ajax request, redirect + if is_ajax: + response = {"url": url} + return JsonResponse(response) + else: + return redirect(url) + + def return_error(self, request, ngo, errors, is_ajax): + if is_ajax: + return JsonResponse(errors) + + context = {} + context["title"] = ngo.name + # make sure the ngo shows a logo + ngo.logo = ngo.logo if ngo.logo else settings.DEFAULT_NGO_LOGO + context["ngo"] = ngo + context["counties"] = settings.LIST_OF_COUNTIES + context["limit"] = settings.DONATIONS_LIMIT - new_donor: Donor = form.save(commit=False) - new_donor.ngo = context["ngo"] + context["errors"] = errors - new_donor.save() + now = timezone.now() + can_donate = not now.date() > settings.DONATIONS_LIMIT - new_donor.pdf_url = self._generate_pdf(form.cleaned_data, context["ngo"]) + context["can_donate"] = can_donate - return redirect(reverse("ngo-twopercent-signature", kwargs={"ngo_url": context["ngo_url"]})) + for key in self.request.POST: + context[key] = self.request.POST[key] - @staticmethod - def _generate_pdf(donor_data, param): - return "PDF_URL" + # render a response + return render(request, self.template_name, context) diff --git a/backend/redirectioneaza/settings.py b/backend/redirectioneaza/settings.py index b93cce85..72a2a4fc 100644 --- a/backend/redirectioneaza/settings.py +++ b/backend/redirectioneaza/settings.py @@ -444,3 +444,6 @@ CAPTCHA_POST_PARAM = env.str("CAPTCHA_POST_PARAM") CAPTCHA_ENABLED = True if CAPTCHA_PUBLIC_KEY else False + + +USER_FORMS = "documents" diff --git a/backend/static_extras/js/twopercent.js b/backend/static_extras/js/twopercent.js index eddcfa9a..4d16cdb5 100644 --- a/backend/static_extras/js/twopercent.js +++ b/backend/static_extras/js/twopercent.js @@ -1,267 +1,232 @@ $(function () { - $('.description').shorten({ - moreText: 'Arată mai mult', - lessText: 'Arată mai puțin', - showChars: 200 - }); - - let errors = { - server_error: "Se pare că am întâmpinat o eroare pe server. Vă rugăm încercați din nou.", - fields_error: "Se pare că următoarele date sunt invalide: " - } - let ngoUrl = window.location.href; - let form = $("#twopercent"); - - let invalidFormAlert = $("#invalid-form-alert"); - let submitFormButton = $("#submit-twopercent-form"); - let signForm = $('#sign-form') - let submitForm = $('.signature-container') - - let cnpField = $("#cnp") - - $('[data-toggle="popover"]').popover({ - trigger: "focus", - placement: ($(window).width() > 790) ? "right" : "bottom" - }); - - let errorClass = "has-error"; - let invalidFields = {}; - - function showError(context) { - invalidFields[context.id] = true; - - let el = $(context); - el.parent().addClass(errorClass); - el.popover({ - content: "Valoarea acestui câmp este invalidă.", - title: "", - placement: ($(window).width() > 790) ? "right" : "bottom", - trigger: "focus" + $('.description').shorten({ + moreText: 'Arata mai mult', + lessText: 'Arata mai putin', + showChars: 200 }); - } - - function hideError(context) { - delete invalidFields[context.id]; - - let el = $(context); - el.parent().removeClass(errorClass); - el.popover('destroy'); - } - - $("#nume, #prenume, #strada, #bloc, #scara, #etaj, #ap, #localitate").on("blur", function () { - let val = this.value; - if (!val) return; - - let regex = /^[\w\s.\-ăîâșțşţ]+$/gi; - // if we have no match - if (!val.match(regex)) { - showError(this); - } else { - hideError(this); - } - }); - - /******************************************************************************/ - /**** Validare CNP ****/ - - /******************************************************************************/ - /** - * Validate CNP ( valid for 1800-2099 ) - * - * @return boolean - * @param p_cnp - */ - // copyright https://github.com/cristian-datu/CNP - function validCNP(p_cnp) { - let i = 0, year = 0, hashResult = 0, cnp = [], hashTable = [2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9]; - if (p_cnp.length !== 13) { - return false; - } - for (i = 0; i < 13; i++) { - cnp[i] = parseInt(p_cnp.charAt(i), 10); - if (isNaN(cnp[i])) { - return false; - } - if (i < 12) { - hashResult = hashResult + (cnp[i] * hashTable[i]); - } - } - hashResult = hashResult % 11; - if (hashResult === 10) { - hashResult = 1; - } - year = (cnp[1] * 10) + cnp[2]; - switch (cnp[0]) { - case 1 : - case 2 : { - year += 1900; - } - break; - case 3 : - case 4 : { - year += 1800; - } - break; - case 5 : - case 6 : { - year += 2000; - } - break; - case 7 : - case 8 : - case 9 : { - year += 2000; - if (year > (parseInt(new Date().getYear(), 10) - 14)) { - year -= 100; - } - } - break; - default : { - return false; - } - } - if (year < 1800 || year > 2099) { - return false; - } - return (cnp[12] === hashResult); - } - - cnpField.on("blur", function () { - let val = this.value; - // if the user provided a value make sure it's valid - if (val && !validCNP(val)) { - showError(this); - } else { - hideError(this); + var errors = { + server_error: "Se pare ca am intampinat o eroare pe server. Va rugam incercati din nou.", + fields_error: "Se pare ca urmatoarele date sunt invalide: " } - }); + var ngoUrl = window.location.href; + var form = $("#twopercent"); - $('#email').on('blur', function () { - let email = $(this).val().trim(); + var invalidFormAlert = $("#invalid-form-alert"); + var submitFormButton = $("#submit-twopercent-form"); + var signForm = $('#sign-form') + var submitForm = $('.signature-container') - let regex = /[\w.-]+@[\w.-]+.\w+/ - if (email && !regex.test(email)) { - showError(this); - } else { - hideError(this); - } - }); + var cnpField = $("#cnp") - $('#telefon').on('blur', function () { - let telefon = $(this).val().trim(); + $('[data-toggle="popover"]').popover({ + trigger: "focus", + placement: ( $(window).width() > 790 ) ? "right" : "bottom" + }); - if (telefon && (telefon.length !== 10 || telefon.slice(0, 2) !== "07")) { - showError(this); - } else { - hideError(this); + var errorClass = "has-error"; + var invalidFields = {}; + function showError (context) { + invalidFields[context.id] = true; + + var el = $(context); + el.parent().addClass(errorClass); + el.popover({ + content: "Valoarea acestui camp este invalida.", + title: "", + placement: ( $(window).width() > 790 ) ? "right" : "bottom", + trigger: "focus" + }); } - }); - - signForm.on('click', function (ev) { - let cnpVal = cnpField.val() - - if (!cnpVal) { - ev.preventDefault() - showError(cnpField) - cnpField.focus() - } else { - if (!validCNP(cnpVal)) { - ev.preventDefault() - showError(cnpField) - cnpField.focus() - } else { - // else, the form will be submitted - hideError(cnpField) - $('').attr('type', 'hidden') - .attr('name', "wants-to-sign").attr('value', 'True') - .appendTo(form); - } - } - }) + function hideError (context) { + delete invalidFields[context.id]; - form.on("submit", function (ev) { - ev.preventDefault(); + var el = $(context); + el.parent().removeClass(errorClass); + el.popover('destroy'); + } - $(this).find("input").blur(); + $("#nume, #prenume, #strada, #bloc, #scara, #etaj, #ap, #localitate").on("blur", function(){ + var val = this.value; + if(!val) return; - let len = 0; - for (let o in invalidFields) { - len++; - } + var regex = /^[\w\s.\-ăîâșțşţ]+$/gi; + // if we have no match + if(!val.match(regex)) { + showError(this); + } else { + hideError(this); + } + }); - if (len === 0) { - // all ok - invalidFormAlert.addClass("hidden"); - } else { - invalidFormAlert.removeClass("hidden"); - return; + /******************************************************************************/ + /**** Validare CNP ****/ + /******************************************************************************/ + /** + * Validate CNP ( valid for 1800-2099 ) + * + * @param string $p_cnp + * @return boolean + */ + // copyright https://github.com/cristian-datu/CNP + function validCNP( p_cnp ) { + var i=0 , year=0 , hashResult=0 , cnp=[] , hashTable=[2,7,9,1,4,6,3,5,8,2,7,9]; + if( p_cnp.length !== 13 ) { return false; } + for( i=0 ; i<13 ; i++ ) { + cnp[i] = parseInt( p_cnp.charAt(i) , 10 ); + if( isNaN( cnp[i] ) ) { return false; } + if( i < 12 ) { hashResult = hashResult + ( cnp[i] * hashTable[i] ); } + } + hashResult = hashResult % 11; + if( hashResult === 10 ) { hashResult = 1; } + year = (cnp[1]*10)+cnp[2]; + switch( cnp[0] ) { + case 1 : case 2 : { year += 1900; } break; + case 3 : case 4 : { year += 1800; } break; + case 5 : case 6 : { year += 2000; } break; + case 7 : case 8 : case 9 : { year += 2000; if( year > ( parseInt( new Date().getYear() , 10 ) - 14 ) ) { year -= 100; } } break; + default : { return false; } + } + if( year < 1800 || year > 2099 ) { return false; } + return ( cnp[12] === hashResult ); } - // add ajax field - $('').attr('type', 'hidden') - .attr('name', "ajax").attr('value', "true") - .appendTo(this); + cnpField.on("blur", function(){ + var val = this.value; + // if the user provided a value make sure it's valid + if( val && !validCNP(val) ) { + showError(this); + } else { + hideError(this); + } + }); - if (grecaptcha && typeof grecaptcha.execute == "function") { - grecaptcha.execute(); - } - }); + $('#email').on('blur', function() { + var email = $(this).val().trim(); - window.onSubmit = function (token) { + var regex = /[\w.-]+@[\w.-]+.\w+/ + if( email && !regex.test(email) ) { + showError(this); + } else { + hideError(this); + } + }); - submitFormButton.removeClass("btn-primary").attr("disabled", true); - signForm.removeClass("btn-primary").attr("disabled", true); + $('#telefon').on('blur', function() { + var telefon = $(this).val().trim(); - $('').attr('type', 'hidden') - .attr('name', "g-recaptcha-response").attr('value', token) - .appendTo(form); + if( telefon && (telefon.length != 10 || telefon.slice(0, 2) !== "07") ) { + showError(this); + } else { + hideError(this); + } + }); + signForm.on('click', function (ev) { + var cnpVal = cnpField.val() - $.ajax({ - url: ngoUrl, - type: "POST", - dataType: "json", - data: form.serialize(), - success: function (data) { - if (data.url) { - window.location = data.url; + if (!cnpVal) { + ev.preventDefault() + showError(cnpField) + cnpField.focus() } else { - message = errors["server_error"]; - submitFormButton.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); - signForm.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); - - invalidFormAlert.removeClass("hidden").find("span").text(message); - grecaptcha.reset(); + if (!validCNP(cnpVal)) { + ev.preventDefault() + showError(cnpField) + cnpField.focus() + } else { + // else, the form will be submitted + hideError(cnpField) + $('').attr('type', 'hidden') + .attr('name', "wants-to-sign").attr('value', 'True') + .appendTo(form); + } } - }, - error: function (data) { + }) - if (grecaptcha && typeof grecaptcha.reset == "function") { - grecaptcha.reset() + form.on("submit", function(ev){ + ev.preventDefault(); + + $(this).find("input").blur(); + + var len = 0; + for (var o in invalidFields) { + len++; } - let response = data.responseJSON; - let message; - if (data.status === 500 || response.server) { - message = errors["server_error"]; - } else if (response.fields && response.fields.length) { - message = errors["fields_error"]; - - for (let field in response.fields) { - message += response.fields + ", "; - } - message = message.slice(0, -2); + if( len == 0 ) { + // all ok + invalidFormAlert.addClass("hidden"); } else { - message = errors["server_error"]; + invalidFormAlert.removeClass("hidden"); + return; } - submitFormButton.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); - invalidFormAlert.removeClass("hidden").find("span").text(message); - } + // add ajax field + $('').attr('type', 'hidden') + .attr('name', "ajax").attr('value', "true") + .appendTo(this); + + + if( grecaptcha && typeof grecaptcha.execute == "function" ) { + grecaptcha.execute(); + } }); - }; -}); + window.onSubmit = function(token) { + + submitFormButton.removeClass("btn-primary").attr("disabled", true); + signForm.removeClass("btn-primary").attr("disabled", true); + + $('').attr('type', 'hidden') + .attr('name', "g-recaptcha-response").attr('value', token) + .appendTo(form); + + + $.ajax({ + url: ngoUrl, + type: "POST", + dataType: "json", + data: form.serialize(), + success: function(data) { + if( data.url ) { + window.location = data.url; + } else { + message = errors["server_error"]; + submitFormButton.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); + signForm.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); + + invalidFormAlert.removeClass("hidden").find("span").text(message); + grecaptcha.reset(); + } + }, + error: function(data) { + + if( grecaptcha && typeof grecaptcha.reset == "function" ) { + grecaptcha.reset() + } + + var response = data.responseJSON; + var message = ""; + if(data.status == 500 || response.server) { + message = errors["server_error"]; + } else if( response.fields && response.fields.length) { + message = errors["fields_error"]; + + for( var field in response.fields ) { + message += response.fields + ", "; + } + message = message.slice(0, -2); + } else { + message = errors["server_error"]; + } + + submitFormButton.addClass("btn-primary").removeClass("btn-success").attr("disabled", false); + invalidFormAlert.removeClass("hidden").find("span").text(message); + } + }); + }; + +}); \ No newline at end of file diff --git a/backend/templates/v1/admin2/accounts.html b/backend/templates/v1/admin2/accounts.html index 4cc49208..71e16c1c 100644 --- a/backend/templates/v1/admin2/accounts.html +++ b/backend/templates/v1/admin2/accounts.html @@ -33,8 +33,8 @@ nu {% endif %} - - {{ "da" if user.verified == True else "nu" }} + + {{ "da" if user.is_verified == True else "nu" }} {% endfor %} diff --git a/backend/templates/v1/admin2/ngos.html b/backend/templates/v1/admin2/ngos.html index a1f64c87..fc7a6375 100644 --- a/backend/templates/v1/admin2/ngos.html +++ b/backend/templates/v1/admin2/ngos.html @@ -24,7 +24,7 @@ {{ loop.index }} - {{ ngo.name }} + {{ ngo.name }} {{ ngo.email or '' }} diff --git a/backend/templates/v1/all-ngos.html b/backend/templates/v1/all-ngos.html index 45bc251b..4aed3439 100644 --- a/backend/templates/v1/all-ngos.html +++ b/backend/templates/v1/all-ngos.html @@ -23,7 +23,7 @@

Asociații pentru care poți redirecționa 3.5%

{% for ngo in ngos %}
- + {% set logo = ngo.logo_url if ngo.logo_url else DEFAULT_NGO_LOGO %} diff --git a/backend/templates/v1/components/ngo-details-form.html b/backend/templates/v1/components/ngo-details-form.html index 62c393be..5a9f7046 100644 --- a/backend/templates/v1/components/ngo-details-form.html +++ b/backend/templates/v1/components/ngo-details-form.html @@ -67,7 +67,7 @@ {# this div is only used to contain the two and to add the has-warning class #}
- +
@@ -111,7 +111,7 @@ {% if is_admin %}
Admin - +
@@ -119,7 +119,7 @@
- +
@@ -133,7 +133,7 @@

Nume: {{ "{0} {1}".decode("utf-8").format(owner.first_name, owner.last_name) }}

Email: {{ owner.email }}

-

Verificat: {{ owner.verified }}

+

Verificat: {{ owner.is_verified }}

Creat la: {{ owner.created.strftime("%d/%m/%Y") }}

diff --git a/backend/templates/v1/components/ngo-header.html b/backend/templates/v1/components/ngo-header.html index 357edfe6..69c2373e 100644 --- a/backend/templates/v1/components/ngo-header.html +++ b/backend/templates/v1/components/ngo-header.html @@ -12,7 +12,7 @@

- + {{ ngo.name }}

@@ -34,7 +34,7 @@

{% endif %} {% if is_admin %} - + Editează ONG-ul {% endif %} diff --git a/backend/templates/v1/email/twopercent-form/twopercent_form.html b/backend/templates/v1/email/twopercent-form/twopercent_form.html index c49b0815..c7dc8f6b 100644 --- a/backend/templates/v1/email/twopercent-form/twopercent_form.html +++ b/backend/templates/v1/email/twopercent-form/twopercent_form.html @@ -313,7 +313,7 @@ - {% if ngo.accepts_forms %} + {% if ngo.is_accepting_forms %} Mai departe, va trebui să printezi și să semnezi acest formular. Apoi, ai următoarele opțiuni: diff --git a/backend/templates/v1/email/twopercent-form/twopercent_form_text.txt b/backend/templates/v1/email/twopercent-form/twopercent_form_text.txt index 89e89943..88721286 100644 --- a/backend/templates/v1/email/twopercent-form/twopercent_form_text.txt +++ b/backend/templates/v1/email/twopercent-form/twopercent_form_text.txt @@ -3,7 +3,7 @@ Descarcă formularul completat de tine {{ form_url }} -{% if ngo.accepts_forms %} +{% if ngo.is_accepting_forms %} Mai departe, va trebui să printezi și să semnezi acest formular. Apoi, ai următoarele opțiuni: - fie îl scanezi și îl trimiți pe e-mail ONG-ului si se ocupa ei de depunerea lui. Adresa de e-mail a ONG-ului este: {{ ngo.email }}; - fie îl trimiți prin curier sau poștă la adresa ONG-ului și se ocupă ei de depunerea lui. Desigur, poți să înmânezi formularul în persoană ONG-ului, dacă e cazul. diff --git a/backend/templates/v1/index.html b/backend/templates/v1/index.html index 6e2ea57b..79fc8967 100644 --- a/backend/templates/v1/index.html +++ b/backend/templates/v1/index.html @@ -66,7 +66,7 @@

{% for ngo in ngos %}
- + {% set logo = ngo.logo_url if ngo.logo_url else DEFAULT_NGO_LOGO %}

@@ -100,38 +100,36 @@

Pasul 1. Completează și descarcă formularu Adresa de domiciliu
- +
- +
- +
- +
- +
- +
- {% with identifier="county" %} - {% with county=county %} + {% with identifier="judet" %} {% include "components/county.html" %} {% endwith %} - {% endwith %}
- +
@@ -178,15 +176,15 @@

Pasul 1. Completează și descarcă formularu
{# if the ngo wants to receive forms through email #} - {% if ngo.accepts_forms %} + {% if ngo.is_accepting_forms %}
Din anul 2022 formularele 230 pot fi depuse online. Dacă vrei să printezi formularul și să îl semnezi, atunci apasă mai jos butonul Descarcă formular și urmează instrucțiunile. Dacă vrei să semnezi și să depui formularul online atunci apasă mai jos Semnează formular și urmează instrucțiunile.
@@ -230,14 +228,14 @@

Se pare că asociația încă nu și-a activat această formă de donație.<

- {% if not errors and ngo.bank_account and ngo.registration_number %} + {% if not errors and ngo.account and ngo.cif %}
{# info about saved data #}
- Notă! Toate datele de pe această pagină sunt transmise folosind o conexiune sigură. Următoarele date sunt stocate temporar, până la completarea formularului: numele, prenumele, orașul, județul, email/telefon. După completarea formularului, aceste date sunt șterse, excepție făcând cazul în care ați optat să permiteți ONG-ului să vă contacteze. În acest caz, datele sunt stocate pe platforma Redirecționează cu scopul de a oferi acces ONG-ului la ele.
- Poți citi mai multe despre Drepturile tale cu privire la protecția datelor.

+ Notă! Toate datele de pe această pagină sunt transmise folosind o conexiune sigură. Următoarele date sunt stocate temporar, până la completarea formularului: numele, prenumele, orasul, judetul, email/telefon. După completarea formularului, aceste date sunt șterse, excepție făcând cazul în care ați optat să permiteți ONG-ului să vă contacteze. În acest caz, datele sunt stocate pe platforma Redirecționează cu scopul de a oferi acces ONG-ului la ele.
+ Poți citi mai multe despre Drepturile tale cu privire la protectia datelor.

Prin completarea acestui formular, redirecționezi 3.5% din impozitul pe venit către organizația pe care ai ales-o.
@@ -274,7 +272,7 @@

Ce reprezintă formularul de 3.5%?

  • Ce se întâmplă cu datele de pe această pagină?

    - Datele de pe această pagină sunt folosite doar pentru a crea fișierul PDF. Doar numele, prenumele, orașul, județul și email-ul/telefonul sunt salvate în baza noastră de date pentru a-i oferi ocazia ONG-ului să intre în legătură cu tine, doar dacă tu decizi acest lucru. + Datele de pe această pagină sunt folosite doar pentru a crea fișierul PDF. Doar numele, prenumele, orașul, județul și email-ul/telefonul sunt salvate în baza noastră de date pentru a-i oferi ocazia ONG-ului să intre în legatură cu tine, doar dacă tu decizi acest lucru.