diff --git a/backend/donations/models.py b/backend/donations/models.py index 7b3c0ab8..05b46fbe 100644 --- a/backend/donations/models.py +++ b/backend/donations/models.py @@ -21,6 +21,11 @@ def ngo_directory_path(subdir, instance, filename) -> str: 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) + + class Ngo(models.Model): # DEFAULT_NGO_LOGO = "https://storage.googleapis.com/redirectioneaza/logo_bw.png" @@ -197,6 +202,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..3107bc29 --- /dev/null +++ b/backend/donations/pdf.py @@ -0,0 +1,298 @@ + +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 + +from logging import info + +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, l in enumerate(ngo_account): + account += l + 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/ngo.py b/backend/donations/views/ngo.py index 2ea2dcff..00600216 100644 --- a/backend/donations/views/ngo.py +++ b/backend/donations/views/ngo.py @@ -1,4 +1,7 @@ +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, HttpRequest, JsonResponse @@ -9,6 +12,7 @@ from .base import BaseHandler from ..forms import DonorInputForm from ..models import Donor, Ngo +from ..pdf import create_pdf class DonationSucces(BaseHandler): @@ -40,7 +44,7 @@ def get(self, request, ngo_url): context["limit"] = DONATION_LIMIT # county = self.donor.county.lower() - # self.template_values["anaf"] = ANAF_OFFICES.get(county, None) + # context["anaf"] = ANAF_OFFICES.get(county, None) # for now, disable showing the ANAF office context["anaf"] = None @@ -78,82 +82,246 @@ def post(self, request, ngo_url): class TwoPercentHandler(BaseHandler): - def get_context_data(self, request: HttpRequest, ngo_url: str): - ngo_url = ngo_url.lower().strip() + template_name = 'twopercent.html' + + def __init__(self, *args, **kwargs): + is_ajax = False + def get(self, request, ngo_url): + try: ngo = Ngo.objects.get(form_url=ngo_url) except Ngo.DoesNotExist: - raise Http404("Nu exista o asociație cu acest URL") + ngo = None + + # if we didn't find it or the ngo doesn't have an active page + if ngo is None or ngo.is_active == False: + 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 + + # 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) + + + if not url_dict.scheme: + url_dict = url_dict._replace(scheme='http') + + + # 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() + + # 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 + + url_dict = url_dict._replace(path='') + + context["ngo_website"] = url_dict.geturl() + else: + raise + + except Exception as e: + + context["ngo_website"] = None + else: - form_counties = settings.FORM_COUNTIES + context["ngo_website"] = None - context = {"is_authenticated": False, "ngo_url": ngo_url, "ngo": ngo, "counties": form_counties} - if request.user.is_authenticated and request.user.ngo == ngo: - context["is_authenticated"] = True - return context + now = timezone.now() + can_donate = not now.date() > settings.DONATIONS_LIMIT - context["limit"] = settings.DONATIONS_LIMIT - context["can_donate"] = True + context["can_donate"] = can_donate + context["is_admin"] = request.user.is_staff # TODO: check this + + return render(request, self.template_name, context) - return context + def post(self, request, ngo_url): + post = self.request.POST + errors = { + "fields": [], + "server": False + } - def get(self, request, ngo_url): - context = self.get_context_data(request, ngo_url) + try: + ngo = Ngo.objects.get(ngo_url=ngo_url) + except Ngo.DoesNotExist: + raise Http404 - template = "twopercent.html" + # if we have an ajax request, just return an answer + is_ajax = post.get("ajax", False) - if context["is_authenticated"]: - template = "ngo/ngo-details.html" + def get_post_value(arg, add_to_error_list=True): + value = post.get(arg) - return render(request, template, context) + # if we received a value + if value: - def post(self, request, ngo_url): - post = request.POST + # 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 "" - context = self.get_context_data(request, ngo_url) + return value + + # the email has the @ so the first regex will fail + elif arg == 'email': - ngo = context["ngo"] - is_ajax = post.get("ajax", False) + # if we found a match + if re.match(r'[^@]+@[^@]+\.[^@]+', value) is not None: + return value + + errors["fields"].append(arg) + return '' - # TODO: Captcha + else: - # 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 + errors["fields"].append(arg) + + elif add_to_error_list: + errors["fields"].append(arg) - form = DonorInputForm(post) - if not form.is_valid(): - context.update(form.cleaned_data) - context["errors"] = {"fields": list(form.errors.values())} + return "" - if is_ajax: - return JsonResponse(context["errors"]) - else: - return render(request, "twopercent.html", context) + donor_dict = {} - donor: Donor = form.save(commit=False) - donor.ngo = ngo - donor.save() + # 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) - request.session["donor_id"] = donor.pk - request.session["has_cnp"] = form.cleaned_data.get("cnp", "") - request.session["signature_required"] = signature_required + donor_dict["email"] = get_post_value("email").lower() + donor_dict["tel"] = get_post_value("tel", False) - # TODO: really create the PDF - donor.pdf_url = self._generate_pdf(form.cleaned_data, context["ngo"]) - donor.save() + 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.accepts_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.account.upper(), + "cif": ngo.cif, + "two_years": two_years, + "special_status": ngo.special_status, + "percent": "3,5%" + } + + if len(errors["fields"]): + self.return_error(ngo, errors, is_ajax) + return + + ## 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}".format(settings.USER_FORMS, str(user_folder), sha1( timezone.now() ).hexdigest()) + + pdf = create_pdf(donor_dict, ngo_data) + + file_url = CloudStorage.save_file(pdf, filename) + + # close the file after it has been uploaded + pdf.close() + + # 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'], + tel = donor_dict['tel'], + anonymous = donor_dict['anonymous'], + two_years = two_years, + income = donor_dict['income'], + # make a request to get geo ip data for this user + geoip = self.get_geoip_data(), + ngo = ngo.key, + pdf_url = file_url, + filename = filename + ) + + donor.put() + + # set the donor id in cookie + request.session["donor_id"] = str(donor.key.id()) + request.session["has_cnp"] = bool(donor_dict["cnp"]) + request.session["signature_required"] = signature_required if not signature_required: # send and email to the donor with a link to the PDF file - - # TODO: Send the actual email - # self.send_email("twopercent-form", donor, self.ngo) - pass + self.send_email("twopercent-form", donor, ngo) url = reverse("ngo-twopercent-success", kwargs={"ngo_url": ngo_url}) if signature_required: @@ -165,8 +333,29 @@ def post(self, request, ngo_url): return JsonResponse(response) else: return redirect(url) + + def return_error(self, 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 + + context["errors"] = errors + + now = timezone.now() + can_donate = not now.date() > settings.DONATIONS_LIMIT + + context["can_donate"] = can_donate + + for key in self.request.POST: + context[ key ] = self.request.POST[ key ] - @staticmethod - def _generate_pdf(donor_data, param): - # TODO: really create the PDF - return "PDF_URL" + # render a response + self.render() 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/templates/v1/twopercent.html b/backend/templates/v1/twopercent.html index 495eb3e4..4a5e1a9e 100644 --- a/backend/templates/v1/twopercent.html +++ b/backend/templates/v1/twopercent.html @@ -60,38 +60,38 @@

Redirecționează în 3 pași simpli

Pasul 1. Completează și descarcă formularul

- {% if ngo.account and ngo.cif %} -
+ {% if ngo.bank_account and ngo.registration_number %} + {{ csrf_input }}
Date personale
- +
- +
- +
- +
- +
- +
@@ -100,25 +100,25 @@

Pasul 1. Completează și descarcă formularu Adresa de domiciliu
- +
- +
- +
- +
- +
- +
@@ -127,7 +127,7 @@

Pasul 1. Completează și descarcă formularu {% include "components/county.html" %}

- +