From 010e1716497a406ae74395f1a0de2d2ed6cb3f24 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:50:51 +0100 Subject: [PATCH 01/10] Completed --- .gitignore | 1 + src/assets/css/main.css | 15 +- src/assets/js/main.js | 46 ++++ src/config/settings/base.py | 27 +- src/mailer/forms.py | 4 +- src/mailer/templates/mailer/base.html | 8 +- src/mailer/templates/mailer/index.html | 147 ++++++----- src/mailer/views.py | 17 +- src/media/x.csv | 344 ------------------------- src/messenger/messager.py | 12 +- test.html | 109 ++++++++ 11 files changed, 294 insertions(+), 436 deletions(-) create mode 100644 src/assets/js/main.js delete mode 100644 src/media/x.csv create mode 100644 test.html diff --git a/.gitignore b/.gitignore index b6e4761..8defd49 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ coverage.xml local_settings.py db.sqlite3 db.sqlite3-journal +media/ # Flask stuff: instance/ diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 41e45a0..70a87dd 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -2,7 +2,7 @@ body { background-color: var(--bs-dark); } -.navbar{ +.navbar { padding-top: 1.5rem; padding-bottom: 1.5rem; background-color: #131518 !important; @@ -13,25 +13,25 @@ body { gap: 1rem; } -.container-fluid{ +.container-fluid { max-width: 900px; margin: 0 auto; } -label{ +label { margin-bottom: 6px; font-weight: 500; } -form{ - max-width: 500px; +form { + max-width: 900px; margin: auto; background: white; padding: 2rem; border-radius: 10px; } -legend{ +legend { margin: 1rem 0 2rem 0; font-weight: bold; } @@ -43,5 +43,4 @@ form button.btn { width: 100%; font-weight: 500; font-size: 1.1rem; -} - +} \ No newline at end of file diff --git a/src/assets/js/main.js b/src/assets/js/main.js new file mode 100644 index 0000000..bff8a97 --- /dev/null +++ b/src/assets/js/main.js @@ -0,0 +1,46 @@ +const fileInput = document.getElementById("csv"); + +const previewCSVData = async (dataurl) => { + const d = await d3.csv(dataurl); + console.log({ + d, + }); + console.log(d.columns); + + const email_key = document.getElementById("email_key"); + d.columns.map((col) => { + email_key.options[email_key.options.length] = new Option(col, col); + }); + + $(function () { + $("#slider-range").slider({ + range: true, + min: 0, + max: d.length, + values: [0, d.length], + slide: function (event, ui) { + $("#start").val(ui.values[0]); + $("#stop").val(ui.values[1]); + }, + }); + $("#start").val($("#slider-range").slider("values", 0)); + $("#stop").val($("#slider-range").slider("values", 1)); + }); +}; + +const readFile = (e) => { + const file = fileInput.files[0]; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = reader.result; + previewCSVData(dataUrl); + }; + reader.readAsDataURL(file); +}; + +if (fileInput) { + fileInput.onchange = readFile; +} diff --git a/src/config/settings/base.py b/src/config/settings/base.py index 2b6fd1d..ddfad26 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -19,7 +19,8 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', - 'mailer' + 'mailer', + 'django_quill', ] MIDDLEWARE = [ @@ -149,3 +150,27 @@ TWILIO_SID = config('TWILIO_SID', default='test') TWILIO_TOKEN = config('TWILIO_TOKEN', default='test') + + +QUILL_CONFIGS = { + 'default':{ + 'theme': 'snow', + 'modules': { + 'syntax': True, + 'toolbar': [ + [ + {'font': []}, + {'header': [1, 2, 3, 4, 5, 6, False]}, + {'align': []}, + 'bold', 'italic', 'underline', 'strike', 'blockquote', + {'color': []}, + {'background': []}, + ], + [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }], + ['code-block', 'link'], + ['clean'], + ] + } + } +} + diff --git a/src/mailer/forms.py b/src/mailer/forms.py index 2ca780f..0f18ea9 100644 --- a/src/mailer/forms.py +++ b/src/mailer/forms.py @@ -1,6 +1,7 @@ from django import forms from django.forms import ValidationError from django.core.validators import FileExtensionValidator, MinValueValidator +from django_quill.forms import QuillFormField from .models import Uploads @@ -17,7 +18,8 @@ class MailForm(forms.Form): sender = forms.CharField() reply_to = forms.EmailField(required=False) subject = forms.CharField() - message = forms.CharField(widget=forms.Textarea) + content = QuillFormField() + email_key = forms.CharField() start = forms.IntegerField(validators=[validator_start_min]) stop = forms.IntegerField(validators=[validator_stop_min]) diff --git a/src/mailer/templates/mailer/base.html b/src/mailer/templates/mailer/base.html index f562fe8..e87b04d 100644 --- a/src/mailer/templates/mailer/base.html +++ b/src/mailer/templates/mailer/base.html @@ -16,7 +16,7 @@ {% endif %} - + @@ -65,18 +65,18 @@ {% block content %} - {% endblock content %} - + + + {% block extra_js %} - {% endblock extra_js %} diff --git a/src/mailer/templates/mailer/index.html b/src/mailer/templates/mailer/index.html index 0e56e5a..32154c3 100644 --- a/src/mailer/templates/mailer/index.html +++ b/src/mailer/templates/mailer/index.html @@ -1,92 +1,105 @@ {% extends 'mailer/base.html' %} {% load mytags %} - {% block content %} -
{% csrf_token %} + {{ form.media }} Send Email -
- - - - {% for error in form.sender.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.reply_to.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.subject.errors %} - {{ error }} - {% endfor %} - -
- -
- - - - {% for error in form.message.errors %} - {{ error }} - {% endfor %} - -
- -
-
- - - - {% for error in form.start.errors %} +
+
+
+
+ + + {% for error in form.sender.errors %} + {{ error }} + {% endfor %} +
+
+ + + {% for error in form.reply_to.errors %} + {{ error }} + {% endfor %} +
+
+ +
+ + + {% for error in form.subject.errors %} {{ error }} - {% endfor %} - + {% endfor %} +
+ +
+ + + + {% for error in form.file.errors %} + {{ error }} + {% endfor %} +
-
- - - - {% for error in form.stop.errors %} +
+
+
+
+ + + {% for error in form.start.errors %} + {{ error }} + {% endfor %} +
+ - +
+ + + {% for error in form.stop.errors %} + {{ error }} + {% endfor %} +
+
+
+
+ +
+ + + {% for error in form.email_key.errors %} {{ error }} - {% endfor %} - + {% endfor %} +
- +
- - - - {% for error in form.file.errors %} - {{ error }} + + {{ form.content }} + {% for error in form.content.errors %} + {{ error }} {% endfor %} -
- + - +
{% endblock content %} - \ No newline at end of file + + +{% block extra_js %} + +{% endblock extra_js %} \ No newline at end of file diff --git a/src/mailer/views.py b/src/mailer/views.py index 0979540..b866a4e 100644 --- a/src/mailer/views.py +++ b/src/mailer/views.py @@ -1,7 +1,11 @@ +import json import os +from typing import Any from django.conf import settings from django.contrib import messages +from django.http import HttpRequest +from django.http.response import HttpResponse from django.shortcuts import redirect from django.views.generic import TemplateView from messenger.email_manager import ZeptoEmailManager @@ -26,6 +30,11 @@ class Dashboard(TemplateView): } form_class = MailForm + def get_context_data(self, **kwargs: Any) -> dict[str, Any]: + context = super().get_context_data(**kwargs) + context['form'] = MailForm() + return context + def get_message_context(self, data: dict): """ Get message context @@ -89,7 +98,8 @@ def create_messenger(self, file_path: str, data: dict): messenger = ExcelMessenger( start=start, stop=stop, - file_path=file_path + file_path=file_path, + recipient_field=data.get('email_key') ) messenger.set_sender_manager( self.create_sender_manager(data) @@ -109,7 +119,7 @@ def post(self, request, **kwargs): obj = form.save() subject = form.cleaned_data.get('subject') - message = form.cleaned_data.get('message') + message = json.loads(form.cleaned_data.get('content')).get('html') start = form.cleaned_data.get('start') stop = form.cleaned_data.get('stop') @@ -121,9 +131,6 @@ def post(self, request, **kwargs): try: messenger = self.create_messenger(file_path, form.cleaned_data) - message = message.replace('\n', '
') - print(message) - sents_fails = messenger.start_process( subject=subject, message=message diff --git a/src/media/x.csv b/src/media/x.csv deleted file mode 100644 index 4f195ac..0000000 --- a/src/media/x.csv +++ /dev/null @@ -1,344 +0,0 @@ -email -abdulwasiikhadijah@gmail.com -oyibopraisegod8@gmail.com -doladapo39@gmail.com -duwagbale07@gmail.com -victofunk21@gmail.com -aderoyinayo07@gmail.com -leni4c.dev@gmail.com -olawalekoladefavour@gmail.com -padekoba@gmail.com -mayowamakinde23@gmail.com -adetunjiolufemijoe@gmail.com -davidolukayode2@gmail.com -akinpeluakinola1@gmail.com -davejoshreal@gmail.com -biggiefrosh@gmail.com -solomonfummy@gmail.com -pasedaiyanuolwa@gmail.com -salimumaddeino14@gmail.com -adejireadegite@gmail.com -sammyadetoye@gmail.com -motunpeculiar@gmail.com -alfredfaith35@gmail.com -olanipekunrhoda@gmail.com -ikeoluwamakinwa73@gmail.com -abdulhakeemyussuf105@gmail.com -emmanuelagboola303@gmail.com -peterauduarms@gmail.com -enochola100@gmail.com -mercymujor@gmail.com -idowublessingj@gmail.com -idowublessingj@gmail.com -aopeyemi072@gmail.com -damilolaawoniran0319@gmail.com -ogundeletimothy360@gmail.com -olatujaoluwaseuncomfort@gmail.com -ayomideadeoye5237@gmail.com -08062850775i@gmail.com -ajokekanbi@gmail.com -heritagesam2013@gmail.com -ajibolamatilda@gmail.com -oluborodevictor110@gmail.com -sammydrillz@gmail.com -tanimomoemmanuel@gmail.com -fidelwole@gmail.com -rodiat815@gmail.com -olamilekandaniel076@gmail.com -adakoleandrew21@gmail.com -xdrhblck@gmail.com -bolarinwadolapo247@gmail.com -ademolaalaba133@gmail.com -olakunleibitoye99@gmail.com -olumideolubosede@gmail.com -quadrinahim9@gmail.com -erumiprosper@gmail.com -oluwadaredorismodupe@gmail.com -ayodejishoga1@gmail.com -adewoleabdulazeez4@gmail.com -olayinkafutty007@gmail.com -sundayg423@gmail.com -ilesanmikolawolee@gmail.com -kymsoftly@gmail.com -sarrstevex@gmail.com -osobaolanrewaju1722@gmail.com -chidiebereangel990@gmail.com -isaolawale4@gmail.com -chukwuajahsabastinedev@gmail.com -timadegboye@gmail.com -ogahnina@gmail.com -timothy.olu.ojo@gmail.com -akinbioyinloluwa@gmail.com -afolamianuoluwapo@gmail.com -olayemieniola321@gmail.com -oluwatobi573@gmail.com -emmaayetan@gmail.com -adiomunirah57@gmail.com -olumomiesther@gmail.com -gbemisolapeace391@gmail.com -brainiachades@gmail.com -qasimrokeeb@gmail.com -miracleadepoju71@gmail.com -pelumilowo@gmail.com -emmanuelid2020@gmail.com -olamilekansam006@gmail.com -iammoses19@gmail.com -emmanuelajibokun9@gmail.com -arowolowahab8@gmail.com -arunaaremun728@gmail.com -tobilobaisreal5@gmail.com -obadaregideon@gmail.com -abiodunade204@gmail.com -mzscripterx5@gmail.com -awwalmustapha41@gmail.com -ibrahimabdulquadri446@gmail.com -ogunsprisy@gmail.com -dadaayomikun11@gmail.com -victorferanmi258@gmail.com -agada838@gmail.com -adubiesuemmanuel@gmail.com -ogunsgen4@gmail.com -dosuprecious87@gmail.com -abioyegreat5@gmail.com -oghenetefa@gmail.com -bolu4ril@gmail.com -ogunremis25@gmail.com -preciousbukunmi07@gmail.com -tobiakin89@gmail.com -okerekefavour96@gmail.com -ejalonibuoluwaseyi@gmail.com -jaygadi13js@gmail.com -mide00237@gmail.com -obariomo@gmail.com -ogunodemarvellous39@gmail.com -ayobamiahmed61@gmail.com -benitaadakeja@gmail.com -showidrees@gmail.com -dapopeace21@gmail.com -geniuspeter96@gmail.com -opeyemiawwal2006@gmail.com -zainabajisafe06@gmail.com -ogunmokunademijucaleb@gmail.com -oluseyeoloruntoba@gmail.com -michealayomide625@gmail.com -dejeremiaholorunyomi@gmail.com -olanehemiah20@gmail.com -ebunoke18@gmail.com -ballicesse@gmail.com -babatopehannah2019@gmail.com -michealonu20@gmail.com -abidogunabidemi52@gmail.com -akinwoleolatope@gmail.com -stellaonyinye80@gmail.com -omotoshoopeyemi2007@gmail.com -michealoluwadamilola456@gmail.com -akindoyinamos9@gmail.com -bellorejoice839@gmail.com -funmilayobisola12@gmail.com -adeyemoesther200@gmail.com -olaniyanbenny01234@gmail.com -contacthezron@gmail.com -apataoyinlade@gmail.com -paulfadayo@gmail.com -enochjulius18@gmail.com -ologunjosh2007@gmail.com -jesubamiseakinyoola@gmail.com -motunrayoakanji1@gmail.com -pelemodesmond14@gmail.com -mazidefied@gmail.com -saheedadedipe@gmail.com -lawrenceoyeniran03@gmail.com -olalusiiyanuoluwa4@gmail.com -danieladebisi77@gmail.com -akingbehinben@gmail.com -easyrilwan@gmail.com -sololadavid4@gmail.com -jumbojnr9@gmail.com -adesanmiadeola18@gmail.com -bakare1234@gmail.com -wealthank2017@gmail.com -motunrayob00@gmail.com -floraibeakaeze@gmail.com -ahmadadewumi@gmail.com -olusegundolapo123@gmail.com -fasasitoluwalase754@gmail.com -danielbrightmonday@gmail.com -dave.400g@gmail.com -adesokanadedeji10@gmail.com -denyefaagbede468@gmail.com -abrahamfolorunso6@gmail.com -akinwandeoyewusi@gmail.com -sunrisetoxscyne777@gmail.com -jattodare002@gmail.com -iamseyisamuel@gmail.com -issactoluwanimi@gmail.com -marhamyusuff24@gmail.com -olanrewajuopeyemi218@gmail.com -idrisade430@gmail.com -timileyindgreat02@gmail.com -ifeolufaro@gmail.com -nadiratraji8806@gmail.com -abdulsemiumaryam60@gmail.com -akinkunmioladosu433@gmail.com -vicolraj@gmail.com -oyeleyebal@gmail.com -bellorejoice839@gmail.com -atopeojo@gmail.com -obariomo@gmail.com -perfectpamilerin@gmail.com -hashrafdeen@gmail.com -mayorfalomo@gmail.com -eyitaina@gmail.com -adexbolaji100@gmail.com -idowujohn248@gmail.com -moyinoluwagrace691@gmail.com -mohammedkamaldeen204@gmail.com -miracle.bonnke@gmail.com -bolajiajiku1111@gmail.com -ogunyemioluwatomilola294@gmail.com -abimbolajoy05@gmail.com -ifeoluwaadebobajo123@gmail.com -ayomoobase51@gmail.com -kehindeolurotimi036@gmail.com -alamuelijah186@gmail.com -olamilekanakintilebo@gmail.com -hammedlawal09@gmail.com -oluwaseunsamuel996@gmail.com -abikedesigns@gmail.com -ewakristiakande01@gmail.com -pasedaiyanuolwa@gmail.com -openthefloggage@gmail.com -victormoseri2005@gmail.com -olatejubabalola4@gmail.com -ogunodemarvellous39@gmail.com -olatujaoluwaseuncomfort@gmail.com -ceo.skyhost@gmail.com -oluwadamilola013@gmail.com -oluwaseunagbamu@gmail.com -graceesther04ever@gmail.com -emechetachristian@gmail.com -ibu809829@gmail.com -rachealomololafalowo@gmail.com -egbayeloayodele72@gmail.com -jaygadi13js@gmail.com -emmanuelaajayi2@gmail.com -dahunsiayomiku@gmail.com -adegboyeoluwatosin98@gmail.com -fagbohunkadominion@gmail.com -samuelamos190@gmail.com -blessingadeniyi793@gmail.com -faithakinboyejo@gmail.com -onitamicheal@gmail.com -okohhenrietta57@gmail.com -gifteleojoonoja@gmail.com -calebclintonadeosun@gmail.com -nwankwokris5@gmail.com -ayomiolaniyan@gmail.com -isaaccassij.omo@gmail.com -sundayk180@gmail.com -maryfamuagun@gmail.com -olayiwolaagbejenbi@gmail.com -adebulejohnfav@gmail.com -olanrewajuopeyemi218@gmail.com -idrisade430@gmail.com -adetoyesesamson@gmail.com -iamirewandemichael@gmail.com -olawunmisamuel104@gmail.com -isewhite1234@gmail.com -peterdarasimiaiku@gmail.com -fakeyejoshua2005@gmail.com -radmakson@gmail.com -lawalgbolahan042@gmail.com -sireadrieldaniel@gmail.com -sololadavid4@gmail.com -koladeodunope@gmail.com -nunsiomi@gmail.com -oyetunjiebunoluwa90@gmail.com -obediencepraise@gmail.com -ademidekoya88@gmail.com -doskiano109@gmail.com -shaymah2002@gmail.com -bakare1234@gmail.com -ojo25879@gmail.com -paulblackk35@gmail.com -imadiyi369@gmail.com -edohblessing98@gmail.com -omoyeniglic@gmail.com -fortuneihean0314@gmail.com -ayoeze191@gmail.com -alamuelijah186@gmail.com -peaceiyanu3010@gmail.com -dosuprecious87@gmail.com -merryola90@gmail.com -akinjogunlamayowa@gmail.com -okeyodedivine2006@gmail.com -adejuwonevidence181@gmail.com -mayowamakinde23@gmail.com -bolajis816@gmail.com -adedejirilwan04@gmail.com -dominioniseoluwa74@gmail.com -abisolamofolasayo@gmail.com -faruqalade6@gmail.com -bamideleisaac2106@gmail.com -adejireadegite@gmail.com -koladekuseju@gmail.com -tomoyeted@gmail.com -abdulhakeemyussuf105@gmail.com -iwayemikehinde1@gmail.com -rahmanomoloja@gmail.com -oluwatofunmijoel765@gmail.com -meshbola15@gmail.com -gbemisolapeace391@gmail.com -oluwasemilogodurojaye@gmail.com -ayomiolaniyan@gmail.com -ibidapoayomide754@gmail.com -kylewebdev0@gmail.com -ajayimichael150@gmail.com -joylafenwa@gmail.com -siyanbolahanifah@gmail.com -toluwanieazi@gmail.com -tobiakin89@gmail.com -okanlawonhammad2018@gmail.com -iamfaymousaisida@gmail.com -femmy0128@gmail.com -josephajayi589@gmail.com -damilareodedina24@gmail.com -olumideologundudu39@gmail.com -croesus245@gmail.com -lekaz22691@gmail.com -olotudav22@gmail.com -abdulrahmanibrahim1405@gmail.com -ibeachusichinazaekpere@gmail.com -ebunoke18@gmail.com -bankolesamuel051@gmail.com -diamondigitals@gmail.com -ifeajagunla@gmail.com -odebodezion@gmail.com -adegboyegunmayowa@gmail.com -victoriamichaels247@gmail.com -boladimeji834@gmail.com -benjaminobadare00@gmail.com -feranmiolasupo203@gmail.com -dondanzy31@gmail.com -deelad25@gmail.com -danieloluwanifesimi9@gmail.com -iwalokun572@gmail.com -kishorekumar200417@gmail.com -iyanujehovahb@gmail.com -dharmielaex@gmail.com -mide00237@gmail.com -akinwoleolatope@gmail.com -boladimeji834@gmail.com -felixgogodae777@gmail.com -depopf89@gmail.com -oluwakewasolaolukoya@gmail.com -mvmonterio@gmail.com -oahdevtech@gmail.com -samueltaiwo856@gmail.com -francisgbohunmi@gmail.com -ejalonibuoluwaseyi@gmail.com -olumideolubosede@gmail.com -johnsonelijahbabs@gmail.com -ajiboyeprecious780@gmail.com -ebinesh200416@gmail.com -damolasanya05@gmail.com \ No newline at end of file diff --git a/src/messenger/messager.py b/src/messenger/messager.py index 159a590..60d51d5 100644 --- a/src/messenger/messager.py +++ b/src/messenger/messager.py @@ -54,12 +54,12 @@ def run_checks(self): class BaseMessenger: def __init__( self, start: int = 0, stop: int = 0, - receipeint_field: str = None + recipient_field: str = None ) -> None: self.__start = start self.__stop = stop self.__manager: Type[Managers] = None - self.__receipeint_field = receipeint_field + self.__recipient_field = recipient_field self.set_manager() self.run_checks() @@ -81,16 +81,16 @@ def get_stop(self) -> int: """ return self.__stop - 1 - def get_receipeint_field(self) -> str: + def get_recipient_field(self) -> str: """ Get receipeint field key :return: receipeint field key :rtype: str """ - if self.__receipeint_field is None: + if self.__recipient_field is None: return self.get_manager().sender_manager.get_receipient_field() - return self.__receipeint_field + return self.__recipient_field def run_checks(self) -> None: if not isinstance(self.__start, int): @@ -314,7 +314,7 @@ def get_index_dict(self, index: int) -> dict: return self.data.loc[index].to_dict() def get_receipient_from_data(self, data: dict) -> str: - key = self.get_receipeint_field() + key = self.get_recipient_field() value = data.get(key) if value is None: raise TypeError(f'{key.capitalize()} column not in file') diff --git a/test.html b/test.html new file mode 100644 index 0000000..79d8dd0 --- /dev/null +++ b/test.html @@ -0,0 +1,109 @@ + + + + + + Email page + + + + + +
+

+ + + + +
+ + + \ No newline at end of file From 88fe4952a9fbb00da957ae831d431102b8e8c892 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:52:00 +0100 Subject: [PATCH 02/10] Fixed test issue --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48987b8..255561b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10"] env: DEBUG: true From 4190ef3720aeff6c7158db668aeb41c658b48b4d Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:54:08 +0100 Subject: [PATCH 03/10] Dev --- .github/workflows/test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 255561b..f4a37bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,9 +5,13 @@ name: Python package on: push: - branches: master + branches: + - dev + - master pull_request: - branches: master + branches: + - dev + - master jobs: build: From 2ee7ad7d9e4d9c9411aa94ef605c59e685d8bdf2 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 04:55:35 +0100 Subject: [PATCH 04/10] update --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4a37bf..50b96b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - cd dry + cd src mkdir logs pytest --cov-report xml - name: Upload Coverage to Codecov From a588a9cf0c5e6b2cf321b6c6d9c45d8387076991 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 05:00:52 +0100 Subject: [PATCH 05/10] Dev --- src/pytest.ini | 2 +- src/tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pytest.ini b/src/pytest.ini index fbbb260..5ea6117 100644 --- a/src/pytest.ini +++ b/src/pytest.ini @@ -1,3 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = dry.settings.test +DJANGO_SETTINGS_MODULE = src.settings.test addopts = --disable-pytest-warnings --cov=. --cov-report term \ No newline at end of file diff --git a/src/tox.ini b/src/tox.ini index 808ae33..c98c57c 100644 --- a/src/tox.ini +++ b/src/tox.ini @@ -1,7 +1,7 @@ [coverage:run] omit = */tests/* manage.py - dry/* + src/* */tests* [coverage:report] From 15e353b93816b63c77e3f2638e494f0dd6c03e99 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 21 Aug 2024 05:14:40 +0100 Subject: [PATCH 06/10] Done --- src/config/settings/base.py | 8 ++++---- src/messenger/email_manager.py | 10 +++++----- src/messenger/messager.py | 8 ++++---- src/messenger/sender_manager.py | 6 +++--- src/messenger/sms_manager.py | 12 ++++++------ src/messenger/tests/test_email_manager.py | 15 +++++++-------- src/messenger/tests/test_messenger.py | 22 +++++++++++----------- src/pytest.ini | 2 +- 8 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/config/settings/base.py b/src/config/settings/base.py index ddfad26..bd4564f 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -125,14 +125,14 @@ 'handlers': { 'basic_h': { 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - # 'filename': BASE_DIR / 'logs/debug.log', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs/debug.log', 'formatter': 'simple', }, 'basic_e': { 'level': 'WARNING', - 'class': 'logging.StreamHandler', - # 'filename': BASE_DIR / 'logs/error.log', + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs/error.log', 'formatter': 'simple', }, }, diff --git a/src/messenger/email_manager.py b/src/messenger/email_manager.py index 194764b..a80c6e7 100644 --- a/src/messenger/email_manager.py +++ b/src/messenger/email_manager.py @@ -19,7 +19,7 @@ def __init__( self.__reply_email = reply_email super().__init__(*args, **kwargs) - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: return settings.RECEIPIENT_EMAIL_KEY def get_reply_email(self) -> str: @@ -120,11 +120,11 @@ def get_post_url(self) -> str: return 'https://api.sendgrid.com/v3/mail/send' def send( - self, receipient: str, subject: str, message: str, **kwargs + self, recipient: str, subject: str, message: str, **kwargs ): response = requests.post( url=self.get_post_url(), - json=self.get_post_data(receipient, subject, message), + json=self.get_post_data(recipient, subject, message), headers=self.get_headers() ) stat = is_success(response.status_code) @@ -219,12 +219,12 @@ def get_post_url(self) -> str: return 'https://api.zeptomail.com/v1.1/email' def send( - self, receipient: str, subject: str, message: str, **kwargs + self, recipient: str, subject: str, message: str, **kwargs ): print(self.get_headers()) response = requests.post( url=self.get_post_url(), - json=self.get_post_data(receipient, subject, message), + json=self.get_post_data(recipient, subject, message), headers=self.get_headers() ) print(response.json()) diff --git a/src/messenger/messager.py b/src/messenger/messager.py index 60d51d5..330f708 100644 --- a/src/messenger/messager.py +++ b/src/messenger/messager.py @@ -89,7 +89,7 @@ def get_recipient_field(self) -> str: :rtype: str """ if self.__recipient_field is None: - return self.get_manager().sender_manager.get_receipient_field() + return self.get_manager().sender_manager.get_recipient_field() return self.__recipient_field def run_checks(self) -> None: @@ -313,7 +313,7 @@ def get_index_dict(self, index: int) -> dict: """ return self.data.loc[index].to_dict() - def get_receipient_from_data(self, data: dict) -> str: + def get_recipient_from_data(self, data: dict) -> str: key = self.get_recipient_field() value = data.get(key) if value is None: @@ -332,9 +332,9 @@ def send_messages( context['message'] = _message _message = self.get_manager()\ .message_manager.render_message(context) - receipient = self.get_receipient_from_data(data) + recipient = self.get_recipient_from_data(data) sent = self.get_manager().sender_manager.send_message( _message, subject=subject, - receipient=receipient + recipient=recipient ) yield sent diff --git a/src/messenger/sender_manager.py b/src/messenger/sender_manager.py index 7136808..0a3e093 100644 --- a/src/messenger/sender_manager.py +++ b/src/messenger/sender_manager.py @@ -14,11 +14,11 @@ def __init__( self.__debug = debug self.__block_send = block_send - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: """ - Get receipient key + Get recipient key """ - raise NotImplementedError('No receipient key') + raise NotImplementedError('No recipient key') def set_debug(self) -> None: self.__debug = True diff --git a/src/messenger/sms_manager.py b/src/messenger/sms_manager.py index 6cc248a..6903626 100644 --- a/src/messenger/sms_manager.py +++ b/src/messenger/sms_manager.py @@ -36,17 +36,17 @@ def __init__( super().__init__(*args, **kwargs) self.client = Client(sid, token) - def get_receipient_field(self) -> str: + def get_recipient_field(self) -> str: return settings.RECEIPIENT_SMS_KEY def send( - self, receipient: str, message: str, **kwargs + self, recipient: str, message: str, **kwargs ) -> bool: """ - Send SMS message to receipient using Twilio + Send SMS message to recipient using Twilio - :param receipient: receipient phone number (e.g. +1234567890) - :type receipient: str + :param recipient: recipient phone number (e.g. +1234567890) + :type recipient: str :param message: message to send :type message: str :return: success of sending message @@ -55,6 +55,6 @@ def send( message = self.client.messages.create( body=message, from_=self.get_sender(), - to=receipient + to=recipient ) return True diff --git a/src/messenger/tests/test_email_manager.py b/src/messenger/tests/test_email_manager.py index 91985d9..87817a5 100644 --- a/src/messenger/tests/test_email_manager.py +++ b/src/messenger/tests/test_email_manager.py @@ -14,8 +14,7 @@ class TestManager(SimpleTestCase): def setUp(self) -> None: manager = BaseEmailManager( - domain='example.com', - sender='test', + sender='test@example.com', ) self.manager = manager @@ -108,8 +107,7 @@ class TestSendGridManager(SimpleTestCase): def setUp(self) -> None: manager = SendGridEmailManager( - domain='example.com', - sender='test', + sender='test@example.com', api_key='test_key', reply_email='noreply@test.io' ) @@ -154,15 +152,16 @@ def test_get_post_url(self): @pytest.mark.xfail def test_send_email(self): - computed = self.manager.send_email( - email='me@gmail.com', + computed = self.manager.send( + recipient='me@gmail.com', subject='testings', message='hello world', ) self.assertFalse(computed) def test_send_email_fail_silently(self): - computed = self.manager.send_email( + computed = self.manager.send( + recipient='me@gmail.com', subject='testings', message='hello world', ) @@ -170,7 +169,7 @@ def test_send_email_fail_silently(self): def test_send_email_not_fail_silently(self): with self.assertRaises(Exception): - self.manager.send_email( + self.manager.send( subject='testings', message='hello world', fail=False diff --git a/src/messenger/tests/test_messenger.py b/src/messenger/tests/test_messenger.py index fac6820..c9518cf 100644 --- a/src/messenger/tests/test_messenger.py +++ b/src/messenger/tests/test_messenger.py @@ -16,18 +16,18 @@ U = Type[TestCase] -def test_set_email_manager(managers: M, email_manager: E): - managers.set_email_manager(email_manager) - assert email_manager is managers.email_manager +def test_set_sender_manager(managers: M, sender_manager: E): + managers.set_sender_manager(sender_manager) + assert sender_manager is managers.sender_manager -def test_set_email_manager_error(managers: M): +def test_set_sender_manager_error(managers: M): with pytest.raises(TypeError): - managers.set_email_manager('wrong type') + managers.set_sender_manager('wrong type') -def test_email_manager(managers: M): - assert managers.email_manager is None +def test_sender_manager(managers: M): + assert managers.sender_manager is None def test_set_message_manager(managers: M, message_manager: T): @@ -52,12 +52,12 @@ def test_run_checks_email(managers: M): managers.run_checks() -def test_run_checks_message(managers: M, email_manager: E): +def test_run_checks_message(managers: M, sender_manager: E): """ raise error for message manager check """ with pytest.raises(NotImplementedError): - managers.set_email_manager(email_manager) + managers.set_sender_manager(sender_manager) managers.run_checks() @@ -100,8 +100,8 @@ def test_base_manager_set_message_manager(messenger: B, message_manager): messenger.set_message_manager(message_manager) -def test_base_manager_set_email_manager(messenger: B, email_manager): - messenger.set_email_manager(email_manager) +def test_base_manager_set_sender_manager(messenger: B, sender_manager): + messenger.set_sender_manager(sender_manager) def test_base_manager_get_message_without_data(messenger: B): diff --git a/src/pytest.ini b/src/pytest.ini index 5ea6117..941c1f5 100644 --- a/src/pytest.ini +++ b/src/pytest.ini @@ -1,3 +1,3 @@ [pytest] -DJANGO_SETTINGS_MODULE = src.settings.test +DJANGO_SETTINGS_MODULE = config.settings.test addopts = --disable-pytest-warnings --cov=. --cov-report term \ No newline at end of file From 6a195e19c748e8ec3351d9804f1990bec1a3b723 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:16:31 +0100 Subject: [PATCH 07/10] Fixed little js and debug --- src/assets/js/main.js | 2 + src/file.csv | 46 +++++++++++++++++-- .../mailer/email_templates/email.html | 8 ---- .../mailer/email_templates/template1.html | 10 ---- src/mailer/views.py | 1 + 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/src/assets/js/main.js b/src/assets/js/main.js index bff8a97..0dd2997 100644 --- a/src/assets/js/main.js +++ b/src/assets/js/main.js @@ -8,6 +8,8 @@ const previewCSVData = async (dataurl) => { console.log(d.columns); const email_key = document.getElementById("email_key"); + // TODO: delete previous options + d.columns.map((col) => { email_key.options[email_key.options.length] = new Option(col, col); }); diff --git a/src/file.csv b/src/file.csv index b114d91..a5fd630 100644 --- a/src/file.csv +++ b/src/file.csv @@ -1,5 +1,41 @@ -first_name,last_name,email -ayo,israel,sketcherslodge@gmail.com -jacob,john,bossdollyp@gmail.com -rita,junior,netrobepy@gmail.com -damian,boy,netrobepy1@gmail.com +email,name +fechnologies@gmail.com,ADENIYI DANIEL +hammedlawal09@gmail.com,Lawal Abdulroqeeb Gbolahan +akinwumiaminatomolola@gmail.com,Aminah Omolola Akinwumi +anoroemmanuel16@gmail.com,Emmanuel Anoro +ejalonibuoluwaseyi@gmail.com,Ejalonibu Samuel Oluwaseyi +ibrahimabdulquadri446@gmail.com,Ibrahim Abdulquadri Abiodun +snwankwo218@gmail.com,Samuel Nwankwo +hesedanu@gmail.com,Afolami Anuoluwapo +dstriker86@gmail.com,Adedeji Ifeoluwa David +davidsonoye@gmail.com,Iseoluwa Oyesanmi +akinluaolorunfunminiyi@gmail.com,Olorunfunminiyi Akinlua +davidakindolani0@gmail.com,Akindolani David oluwasegun +rabiu2993@gmail.com,Rabiu Ahmad Omotoyosi +akinleyeatanda6@gmail.com,AKINLEYE LEKAN DANIEL +alfredfaith35@gmail.com,Alfred Faith +johnsonolaiyanu@gmail.com,Olaleye Iyanu Johnson +adejuwonevidence181@gmail.com,Evidence Adejuwon +osenibunmi2023@gmail.com,Oseni Oluwabunmi +akanbiazeez117@gmail.com,Akanbi Abdulazeez Kolawole +joshuaomisanya41@gmail.com,Joshua omisanya Ayomiposi +dominioniseoluwa74@gmail.com,Oluwanimotele Dominion +lolooyin@gmail.com,Apata Oyinlade +jesukoladeisaac@gmail.com,JESUKOLADE ISAAC OLUWASEGUNFUNMI +estheroyewusi436@gmail.com,Oyewusi Esther Adedoyin +shaymah2002@gmail.com,Adeyemi shaymah oyindamola +snwankwo218@gmail.com,Samuel Nwankwo +sololadavid4@gmail.com,Solola Oluwanifemi David +nunsiomi@gmail.com,Oladepo Oluwaferanmi and Shiaki Nunsi +dan40ricky@gmail.com,James Ayomide +thejamesnick@gmail.com,Nicholas James +nunsiomi@gmail.com,Nunsi +chukwuajahsabastinedev@gmail.com,CHUKWU AJAH SABASTINE +jimijay.oj@gmail.com,Murphy Blackman +yekinirasheed2002@gmail.com,Rasheed Yekini +oluwatomisinrapheal12@gmail.com,Rapheal oluwatomisin A +akeebrasheedat2018@gmail.com,Akeeb Rasheedat Anike +osasodiasea1@gmail.com,Odiase osarumwense +ogunodemarvellous39@gmail.com,Ogunode marvellous +durallite@gmail.com,Aduragbemi ademola +oluwadamilola013@gmail.com,Osunsanwo Damilola \ No newline at end of file diff --git a/src/mailer/templates/mailer/email_templates/email.html b/src/mailer/templates/mailer/email_templates/email.html index 231df7d..1ca74d3 100644 --- a/src/mailer/templates/mailer/email_templates/email.html +++ b/src/mailer/templates/mailer/email_templates/email.html @@ -85,14 +85,6 @@

- - {% block footer %} - - {% endblock footer %} - -

diff --git a/src/mailer/templates/mailer/email_templates/template1.html b/src/mailer/templates/mailer/email_templates/template1.html index 8d5733e..4ebb1eb 100644 --- a/src/mailer/templates/mailer/email_templates/template1.html +++ b/src/mailer/templates/mailer/email_templates/template1.html @@ -5,13 +5,3 @@ {{ message|safe }} {% endblock contents %} - - -{% block footer %} - -

Best regards,

-

Ayanwola Ayomide

-

GDSC Lead

-

TechFest 24 Organizing Team

- -{% endblock footer %} \ No newline at end of file diff --git a/src/mailer/views.py b/src/mailer/views.py index b866a4e..e00aca8 100644 --- a/src/mailer/views.py +++ b/src/mailer/views.py @@ -152,6 +152,7 @@ def post(self, request, **kwargs): if completed: return redirect(self.request.get_full_path()) + print(form.errors) context = self.get_context_data(form=form) return self.render_to_response(context) From 52f655e199bea00963218e34b2225d06a8636891 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Sat, 4 Jan 2025 20:11:21 +0100 Subject: [PATCH 08/10] Added smtp email manager --- .gitignore | 2 + .vscode/settings.json | 5 +- src/assets/js/main.js | 5 +- src/config/settings/dev.py | 6 ++ src/file.csv | 41 ------- src/mailer/forms.py | 1 + src/mailer/templates/mailer/index.html | 11 +- src/mailer/views.py | 10 ++ src/messenger/email_manager.py | 141 +++++++++++++++++-------- src/messenger/messager.py | 5 +- src/messenger/smtp.py | 106 +++++++++++++++++++ src/utils/general.py | 4 + 12 files changed, 245 insertions(+), 92 deletions(-) delete mode 100644 src/file.csv create mode 100644 src/messenger/smtp.py diff --git a/.gitignore b/.gitignore index 8defd49..f3b3cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,5 @@ dmypy.json # Pyre type checker .pyre/ + +*.csv \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index d81e1af..071caa1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "autoDocstring.docstringFormat": "sphinx", - "editor.tabSize": 4 + "editor.tabSize": 4, + "cSpell.words": [ + "sents" + ] } \ No newline at end of file diff --git a/src/assets/js/main.js b/src/assets/js/main.js index 0dd2997..62b1f16 100644 --- a/src/assets/js/main.js +++ b/src/assets/js/main.js @@ -8,8 +8,9 @@ const previewCSVData = async (dataurl) => { console.log(d.columns); const email_key = document.getElementById("email_key"); - // TODO: delete previous options - + // delete previous options + email_key.options.length = 0; + d.columns.map((col) => { email_key.options[email_key.options.length] = new Option(col, col); }); diff --git a/src/config/settings/dev.py b/src/config/settings/dev.py index 2b07311..4bc0d03 100644 --- a/src/config/settings/dev.py +++ b/src/config/settings/dev.py @@ -9,3 +9,9 @@ EMAIL_DOMAIN = config('EMAIL_DOMAIN', default='test') BLOCK_EMAIL = config('BLOCK_EMAIL', default=True, cast=bool) DEBUG_EMAIL = config('DEBUG_EMAIL', default=True, cast=bool) + + +EMAIL_USER = config('EMAIL_USER', default='test') +EMAIL_PASSWORD = config('EMAIL_PASSWORD', default='test') +EMAIL_HOST = config('EMAIL_HOST', default='test') +EMAIL_PORT = config('EMAIL_PORT', default='test', cast=int) diff --git a/src/file.csv b/src/file.csv deleted file mode 100644 index a5fd630..0000000 --- a/src/file.csv +++ /dev/null @@ -1,41 +0,0 @@ -email,name -fechnologies@gmail.com,ADENIYI DANIEL -hammedlawal09@gmail.com,Lawal Abdulroqeeb Gbolahan -akinwumiaminatomolola@gmail.com,Aminah Omolola Akinwumi -anoroemmanuel16@gmail.com,Emmanuel Anoro -ejalonibuoluwaseyi@gmail.com,Ejalonibu Samuel Oluwaseyi -ibrahimabdulquadri446@gmail.com,Ibrahim Abdulquadri Abiodun -snwankwo218@gmail.com,Samuel Nwankwo -hesedanu@gmail.com,Afolami Anuoluwapo -dstriker86@gmail.com,Adedeji Ifeoluwa David -davidsonoye@gmail.com,Iseoluwa Oyesanmi -akinluaolorunfunminiyi@gmail.com,Olorunfunminiyi Akinlua -davidakindolani0@gmail.com,Akindolani David oluwasegun -rabiu2993@gmail.com,Rabiu Ahmad Omotoyosi -akinleyeatanda6@gmail.com,AKINLEYE LEKAN DANIEL -alfredfaith35@gmail.com,Alfred Faith -johnsonolaiyanu@gmail.com,Olaleye Iyanu Johnson -adejuwonevidence181@gmail.com,Evidence Adejuwon -osenibunmi2023@gmail.com,Oseni Oluwabunmi -akanbiazeez117@gmail.com,Akanbi Abdulazeez Kolawole -joshuaomisanya41@gmail.com,Joshua omisanya Ayomiposi -dominioniseoluwa74@gmail.com,Oluwanimotele Dominion -lolooyin@gmail.com,Apata Oyinlade -jesukoladeisaac@gmail.com,JESUKOLADE ISAAC OLUWASEGUNFUNMI -estheroyewusi436@gmail.com,Oyewusi Esther Adedoyin -shaymah2002@gmail.com,Adeyemi shaymah oyindamola -snwankwo218@gmail.com,Samuel Nwankwo -sololadavid4@gmail.com,Solola Oluwanifemi David -nunsiomi@gmail.com,Oladepo Oluwaferanmi and Shiaki Nunsi -dan40ricky@gmail.com,James Ayomide -thejamesnick@gmail.com,Nicholas James -nunsiomi@gmail.com,Nunsi -chukwuajahsabastinedev@gmail.com,CHUKWU AJAH SABASTINE -jimijay.oj@gmail.com,Murphy Blackman -yekinirasheed2002@gmail.com,Rasheed Yekini -oluwatomisinrapheal12@gmail.com,Rapheal oluwatomisin A -akeebrasheedat2018@gmail.com,Akeeb Rasheedat Anike -osasodiasea1@gmail.com,Odiase osarumwense -ogunodemarvellous39@gmail.com,Ogunode marvellous -durallite@gmail.com,Aduragbemi ademola -oluwadamilola013@gmail.com,Osunsanwo Damilola \ No newline at end of file diff --git a/src/mailer/forms.py b/src/mailer/forms.py index 0f18ea9..a6ea0cb 100644 --- a/src/mailer/forms.py +++ b/src/mailer/forms.py @@ -22,6 +22,7 @@ class MailForm(forms.Form): email_key = forms.CharField() start = forms.IntegerField(validators=[validator_start_min]) stop = forms.IntegerField(validators=[validator_stop_min]) + attachments = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False) def clean(self): cleaned = super().clean() diff --git a/src/mailer/templates/mailer/index.html b/src/mailer/templates/mailer/index.html index 32154c3..13fc995 100644 --- a/src/mailer/templates/mailer/index.html +++ b/src/mailer/templates/mailer/index.html @@ -41,13 +41,22 @@
- + {% for error in form.file.errors %} {{ error }} {% endfor %}
+ +
+ + + + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
diff --git a/src/mailer/views.py b/src/mailer/views.py index e00aca8..46bd0fc 100644 --- a/src/mailer/views.py +++ b/src/mailer/views.py @@ -123,6 +123,15 @@ def post(self, request, **kwargs): start = form.cleaned_data.get('start') stop = form.cleaned_data.get('stop') + attachments = [] + files = request.FILES.getlist('attachments') + for file in files: + attachments.append({ + 'filename': file.name, + 'data': file.read(), + "mime_type": file.content_type + }) + file_path = obj.file.path messages_to_be_sent = stop - start + 1 @@ -147,6 +156,7 @@ def post(self, request, **kwargs): err_logger.exception(e) messages.warning(request, e) + raise Exception(response_message) obj.delete() os.remove(file_path) if completed: diff --git a/src/messenger/email_manager.py b/src/messenger/email_manager.py index a80c6e7..04d88f5 100644 --- a/src/messenger/email_manager.py +++ b/src/messenger/email_manager.py @@ -6,16 +6,15 @@ import requests from django.conf import settings -from utils.general import is_success +from messenger.smtp import EmailConnection, get_connection +from utils.general import bytes_to_base64_str, is_success from utils.loggers import logger from .sender_manager import BaseSenderManager class BaseEmailManager(BaseSenderManager): - def __init__( - self, reply_email: str = None, *args, **kwargs - ) -> None: + def __init__(self, reply_email: str = None, *args, **kwargs) -> None: self.__reply_email = reply_email super().__init__(*args, **kwargs) @@ -35,17 +34,12 @@ def get_reply_email(self) -> str: class SendGridEmailManager(BaseEmailManager): - @overload def __init__( - self, api_key: str, sender: str, debug: bool, - block_send: bool, reply_email: str - ) -> None: - ... + self, api_key: str, sender: str, debug: bool, block_send: bool, reply_email: str + ) -> None: ... - def __init__( - self, api_key: str, *args, **kwargs - ) -> None: + def __init__(self, api_key: str, *args, **kwargs) -> None: """ SendGrid email manager @@ -72,9 +66,7 @@ def set_headers(self) -> dict: :return: headers :rtype: dict """ - headers = { - 'Authorization': f'Bearer {self.get_api_key()}' - } + headers = {"Authorization": f"Bearer {self.get_api_key()}"} return headers def get_headers(self) -> dict: @@ -86,9 +78,7 @@ def get_headers(self) -> dict: """ return self.__headers - def get_post_data( - self, email: str, subject: str, message: str - ) -> Dict[str, str]: + def get_post_data(self, email: str, subject: str, message: str) -> Dict[str, str]: """ Get post data for sendgrid @@ -106,7 +96,7 @@ def get_post_data( "from": {"email": self.get_sender()}, "reply_to": {"email": self.get_reply_email()}, "subject": subject, - "content": [{"type": "text/html", "value": message}] + "content": [{"type": "text/html", "value": message}], } return data @@ -117,15 +107,13 @@ def get_post_url(self) -> str: :return: post url :rtype: str """ - return 'https://api.sendgrid.com/v3/mail/send' + return "https://api.sendgrid.com/v3/mail/send" - def send( - self, recipient: str, subject: str, message: str, **kwargs - ): + def send(self, recipient: str, subject: str, message: str, **kwargs): response = requests.post( url=self.get_post_url(), json=self.get_post_data(recipient, subject, message), - headers=self.get_headers() + headers=self.get_headers(), ) stat = is_success(response.status_code) if not stat and self.get_debug(): @@ -135,17 +123,12 @@ def send( class ZeptoEmailManager(BaseEmailManager): - @overload def __init__( - self, api_key: str, sender: str, debug: bool, - block_send: bool, reply_email: str - ) -> None: - ... + self, api_key: str, sender: str, debug: bool, block_send: bool, reply_email: str + ) -> None: ... - def __init__( - self, api_key: str, *args, **kwargs - ) -> None: + def __init__(self, api_key: str, *args, **kwargs) -> None: """ Zepto email manager @@ -172,9 +155,7 @@ def set_headers(self) -> dict: :return: headers :rtype: dict """ - headers = { - 'Authorization': self.get_api_key() - } + headers = {"Authorization": self.get_api_key()} return headers def get_headers(self) -> dict: @@ -187,7 +168,7 @@ def get_headers(self) -> dict: return self.__headers def get_post_data( - self, email: str, subject: str, message: str + self, email: str, subject: str, message: str, **kwargs ) -> Dict[str, str]: """ Get post data for zeptomail @@ -201,11 +182,20 @@ def get_post_data( :return: post data :rtype: Dict[str, str] """ + attachments = kwargs.get("attachments", []) data = { "to": [{"email_address": {"address": email}}], "from": {"address": self.get_sender()}, "subject": subject, - "htmlbody": message + "htmlbody": message, + "attachments": [ + { + "name": _["filename"], + "content": bytes_to_base64_str(_["data"]), + "mime_type": _["mime_type"], + } + for _ in attachments + ], } return data @@ -216,20 +206,81 @@ def get_post_url(self) -> str: :return: post url :rtype: str """ - return 'https://api.zeptomail.com/v1.1/email' + return "https://api.zeptomail.com/v1.1/email" - def send( - self, recipient: str, subject: str, message: str, **kwargs - ): - print(self.get_headers()) + def send(self, recipient: str, subject: str, message: str, **kwargs): response = requests.post( url=self.get_post_url(), - json=self.get_post_data(recipient, subject, message), - headers=self.get_headers() + json=self.get_post_data(recipient, subject, message, **kwargs), + headers=self.get_headers(), ) - print(response.json()) stat = is_success(response.status_code) if not stat and self.get_debug(): logger.debug(response.content) logger.debug(response.status_code) return stat + + +class SmtpEmailManager(BaseEmailManager): + @overload + def __init__( + self, + host: str, + port: int, + username: str, + password: str, + sender: str, + debug: bool, + block_send: bool, + reply_email: str, + ) -> None: ... + + def __init__( + self, host: str, port: int, username: str, password: str, *args, **kwargs + ) -> None: + """ + SMTP email manager + + :param host: host + :type host: str + :param port: port + :type port: int + :param username: username + :type username: str + :param password: password + :type password: str + """ + super().__init__(*args, **kwargs) + self.__host = host + self.__port = port + self.__username = username + self.__password = password + + @property + def conn(self) -> EmailConnection: + """ + Get SMTP connection + """ + return get_connection( + host=self.__host, + port=self.__port, + username=self.__username, + password=self.__password, + ) + + def send( + self, recipient: str, subject: str, message: str, attachments=None, **kwargs + ): + try: + self.conn.send( + subject=subject, + recipient=recipient, + text=message, + sender=self.get_sender(), + html=message, + attachments=attachments, + ) + except Exception as e: + logger.exception(e) + return False + return True diff --git a/src/messenger/messager.py b/src/messenger/messager.py index 330f708..10a1134 100644 --- a/src/messenger/messager.py +++ b/src/messenger/messager.py @@ -322,7 +322,7 @@ def get_recipient_from_data(self, data: dict) -> str: def send_messages( self, subject: str, message: str, - context: dict = None + context: dict = None, **kwargs ): if context is None: context = {} @@ -335,6 +335,7 @@ def send_messages( recipient = self.get_recipient_from_data(data) sent = self.get_manager().sender_manager.send_message( _message, subject=subject, - recipient=recipient + recipient=recipient, + **kwargs ) yield sent diff --git a/src/messenger/smtp.py b/src/messenger/smtp.py new file mode 100644 index 0000000..524c02e --- /dev/null +++ b/src/messenger/smtp.py @@ -0,0 +1,106 @@ +import smtplib +import ssl +from contextlib import ContextDecorator +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email import encoders +from email.mime.base import MIMEBase +import logging + + +class EmailConnection(ContextDecorator): + """ + This class is responsible for connecting to the + email server and sending the email. + """ + + def __init__(self, host: str, port: int, username: str, password: str): + self.host = host + self.port = port + self.username = username + self.password = password + self.connection = None + self.context = ssl.create_default_context() + + def __enter__(self): + logging.info("Connecting to the email server") + if self.port == 465: + self.connection = smtplib.SMTP_SSL( + self.host, self.port, context=self.context + ) + else: + self.connection = smtplib.SMTP(self.host, self.port) + self.connection.starttls(context=self.context) + logging.info("Logging into the email server") + self.connection.login(self.username, self.password) + logging.info("Successfully logged in") + return self + + def __exit__(self, *exc): + logging.info("Disconnecting from the email server") + self.connection.quit() + + def send( + self, + subject: str, + recipient: str, + text: str, + sender: str, + html: str = None, + attachments: list[dict[str, bytes | str]] = None, + ): + """ + Send email + + :param subject: Email subject + :type subject: str + :param recipient: Email recipient + :type recipient: str + :param text: Email text + :type text: str + :param sender: Email sender + :type sender: str + :param html: Email html, defaults to None + :type html: str, optional + :param attachments: Email attachments in the form of a list of dictionaries of the form {"filename": str, "data": bytes}, defaults to None + :type attachments: list[dict[str, bytes | str]], optional + :return: True if email is sent successfully else raise exception + :rtype: bool + """ + message = MIMEMultipart("alternative") + message["Subject"] = subject + message["From"] = sender + message["To"] = recipient + + message.attach(MIMEText(text, "plain")) + if html is not None: + message.attach(MIMEText(html, "html")) + + if attachments is not None: + for attachment in attachments: + part = MIMEBase("application", "octet-stream") + part.set_payload(attachment["data"]) + encoders.encode_base64(part) + filename = attachment["filename"] + part.add_header( + "Content-Disposition", + f"attachment; filename= {filename}", + ) + message.attach(part) + + try: + self.connection.sendmail(self.username, recipient, message.as_string()) + return True + except Exception as e: + logging.error(f"Failed to send email: {e}") + raise e + + +connection: EmailConnection = None + + +def get_connection(host: str, port: int, username: str, password: str): + global connection + if connection is None: + connection = EmailConnection(host, port, username, password) + return connection diff --git a/src/utils/general.py b/src/utils/general.py index 6450c81..fca2833 100644 --- a/src/utils/general.py +++ b/src/utils/general.py @@ -1,6 +1,7 @@ """General python utilities to be used in project""" +import base64 from typing import Any, Iterable @@ -18,3 +19,6 @@ def count_true_in_iter(iter): def is_success(code): return 200 <= code <= 299 + +def bytes_to_base64_str(data: bytes) -> str: + return base64.b64encode(data).decode('utf-8') From 413ee7aa35a43df8519cabc4f14086f2981e86ee Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Sun, 5 Jan 2025 11:32:37 +0100 Subject: [PATCH 09/10] Added dynamic email config --- .vscode/settings.json | 3 +- src/assets/css/main.css | 40 ++++- src/mailer/forms.py | 48 ++++- src/mailer/mail_manager.py | 64 +++++++ src/mailer/migrations/0002_emailmanager.py | 22 +++ .../0003_alter_emailmanager_mail_manager.py | 18 ++ src/mailer/models.py | 37 ++++ src/mailer/templates/mailer/base.html | 7 + src/mailer/templates/mailer/index.html | 14 +- src/mailer/templates/mailer/settings.html | 168 ++++++++++++++++++ src/mailer/templates/mailer/sms.html | 2 +- src/mailer/urls.py | 3 +- src/mailer/views.py | 150 ++++++++++------ src/messenger/email_manager.py | 4 +- src/messenger/smtp.py | 40 +++-- 15 files changed, 531 insertions(+), 89 deletions(-) create mode 100644 src/mailer/mail_manager.py create mode 100644 src/mailer/migrations/0002_emailmanager.py create mode 100644 src/mailer/migrations/0003_alter_emailmanager_mail_manager.py create mode 100644 src/mailer/templates/mailer/settings.html diff --git a/.vscode/settings.json b/.vscode/settings.json index 071caa1..be6581f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "autoDocstring.docstringFormat": "sphinx", "editor.tabSize": 4, "cSpell.words": [ - "sents" + "sents", + "Zepto" ] } \ No newline at end of file diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 70a87dd..c225b86 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -23,7 +23,7 @@ label { font-weight: 500; } -form { +.box-container { max-width: 900px; margin: auto; background: white; @@ -36,11 +36,45 @@ legend { font-weight: bold; } -form button.btn { +#sender button.btn { margin-top: 3rem !important; display: block; padding: 1.1rem 2rem; width: 100%; font-weight: 500; font-size: 1.1rem; -} \ No newline at end of file +} + + +.manager-form { + display: flex; + gap: 12px; + flex-wrap: wrap; + background: #212529; + color: white; + padding: 1rem; + border-radius: 12px; +} + +.manager-form p { + flex: 1; + min-width: 200px; +} + + +#manager-details { + display: flex; + gap: 12px; + flex-wrap: wrap; +} +#manager-details .manager-detail-item { + flex: 1; + min-width: 200px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} +#manager-details .manager-detail-item strong { + font-weight: 500; + text-transform: capitalize; +} diff --git a/src/mailer/forms.py b/src/mailer/forms.py index a6ea0cb..85ada87 100644 --- a/src/mailer/forms.py +++ b/src/mailer/forms.py @@ -3,11 +3,13 @@ from django.core.validators import FileExtensionValidator, MinValueValidator from django_quill.forms import QuillFormField -from .models import Uploads +from mailer.mail_manager import MAIL_MANAGERS + +from .models import EmailManager, Uploads validator_file_ext = FileExtensionValidator( - allowed_extensions=['xls', 'gz', 'csv', 'xlsx'] + allowed_extensions=["xls", "gz", "csv", "xlsx"] ) validator_start_min = MinValueValidator(limit_value=1) validator_stop_min = MinValueValidator(limit_value=1) @@ -22,25 +24,53 @@ class MailForm(forms.Form): email_key = forms.CharField() start = forms.IntegerField(validators=[validator_start_min]) stop = forms.IntegerField(validators=[validator_stop_min]) - attachments = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}), required=False) + attachments = forms.FileField( + widget=forms.ClearableFileInput(attrs={"multiple": True}), required=False + ) + mail_manager = forms.ModelChoiceField( + queryset=EmailManager.objects.all(), + empty_label=None, + widget=forms.Select(attrs={"class": "form-select"}), + required=True, + ) def clean(self): cleaned = super().clean() # Validate stop is greater than start - start = cleaned.get('start') - stop = cleaned.get('stop') + start = cleaned.get("start") + stop = cleaned.get("stop") if (start is not None and stop is not None) and (start > stop): error = ValidationError( - 'Start must not be greater that stop', - code='start_stop_error') - self.add_error('start', error) + "Start must not be greater that stop", code="start_stop_error" + ) + self.add_error("start", error) return cleaned def save(self, commit=True): - file = self.cleaned_data.get('file') + file = self.cleaned_data.get("file") obj = Uploads(file=file) if commit: obj.save() return obj + + +class EmailManagerConfigForm(forms.ModelForm): + label = forms.CharField( + widget=forms.TextInput(attrs={"class": "form-control"}), + help_text="Enter a label for this email manager", + ) + mail_manager = forms.ChoiceField( + choices=MAIL_MANAGERS, + widget=forms.Select(attrs={"class": "form-select"}), + help_text="Select the email manager", + ) + + class Meta: + model = EmailManager + fields = ["label", "mail_manager"] + + def save(self, config, *args, **kwargs): + self.instance.config = config + return super().save(*args, **kwargs) diff --git a/src/mailer/mail_manager.py b/src/mailer/mail_manager.py new file mode 100644 index 0000000..49a15c9 --- /dev/null +++ b/src/mailer/mail_manager.py @@ -0,0 +1,64 @@ +from django import forms +import socket + +from messenger.email_manager import SendGridEmailManager, SmtpEmailManager, ZeptoEmailManager + + +class ManagerForm(forms.Form): + manager = None + + def __init__(self, manager: str = None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.manager = manager + + +class SMTPForm(ManagerForm): + host = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + port = forms.IntegerField(widget=forms.NumberInput(attrs={"class": "form-control"})) + username = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + password = forms.CharField( + widget=forms.PasswordInput(attrs={"class": "form-control"}) + ) + + def clean_host(self): + host = self.cleaned_data.get("host") + try: + # Validate the host by resolving it + socket.gethostbyname(host) + except socket.error: + raise forms.ValidationError( + "Invalid host. Please enter a valid hostname or IP address." + ) + return host + + def clean_port(self): + port = self.cleaned_data.get("port") + if port < 1 or port > 65535: + raise forms.ValidationError("Port must be between 1 and 65535.") + return port + + +class ApiKeyForm(ManagerForm): + api_key = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + + +MAIL_MANAGERS = ( + ("sendgrid", "Sendgrid"), + ("zepto", "ZeptoMail"), + ("smtp", "SMTP Server"), +) + +MANAGER_CONFIG = { + "sendgrid": { + "form": ApiKeyForm, + "manager": SendGridEmailManager, + }, + "zepto": { + "form": ApiKeyForm, + "manager": ZeptoEmailManager, + }, + "smtp": { + "form": SMTPForm, + "manager": SmtpEmailManager + }, +} diff --git a/src/mailer/migrations/0002_emailmanager.py b/src/mailer/migrations/0002_emailmanager.py new file mode 100644 index 0000000..a2b6b5e --- /dev/null +++ b/src/mailer/migrations/0002_emailmanager.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.5 on 2025-01-04 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EmailManager', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=255)), + ('mail_manager', models.CharField(choices=[('sendgrid', 'SendgridEmailManager'), ('zepto', 'ZeptoEmailManager'), ('smtp', 'SMTPEmailManager')], max_length=10)), + ('config', models.JSONField()), + ], + ), + ] diff --git a/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py b/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py new file mode 100644 index 0000000..f000872 --- /dev/null +++ b/src/mailer/migrations/0003_alter_emailmanager_mail_manager.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.5 on 2025-01-04 20:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailer', '0002_emailmanager'), + ] + + operations = [ + migrations.AlterField( + model_name='emailmanager', + name='mail_manager', + field=models.CharField(choices=[('sendgrid', 'Sendgrid'), ('zepto', 'ZeptoMail'), ('smtp', 'SMTP Server')], max_length=10), + ), + ] diff --git a/src/mailer/models.py b/src/mailer/models.py index 17dd0a1..21cc064 100644 --- a/src/mailer/models.py +++ b/src/mailer/models.py @@ -1,5 +1,10 @@ +import json from django.db import models +from mailer.mail_manager import MAIL_MANAGERS, MANAGER_CONFIG +from messenger.email_manager import BaseEmailManager +from django.conf import settings + class Uploads(models.Model): """ @@ -10,7 +15,39 @@ class Uploads(models.Model): :return: Uploads :rtype: models.Model """ + file = models.FileField() def __str__(self): return self.file.name + + +class EmailManager(models.Model): + label = models.CharField(max_length=255) + mail_manager = models.CharField(max_length=10, choices=MAIL_MANAGERS) + config = models.JSONField() + + @property + def config_json(self): + return json.dumps( + { + "label": self.label, + "mail_manager": self.get_mail_manager_display(), + **self.config, + } + ) + + def get_email_manager(self, sender: str, reply_to: str) -> BaseEmailManager: + email_manager: BaseEmailManager = MANAGER_CONFIG.get(self.mail_manager).get( + "manager" + ) + return email_manager( + **self.config, + sender=f"{sender}@{settings.EMAIL_DOMAIN}", + block_send=settings.BLOCK_EMAIL, + debug=settings.DEBUG_EMAIL, + reply_email=reply_to, + ) + + def __str__(self): + return f"{self.label} - {self.get_mail_manager_display()}" diff --git a/src/mailer/templates/mailer/base.html b/src/mailer/templates/mailer/base.html index e87b04d..ae4343e 100644 --- a/src/mailer/templates/mailer/base.html +++ b/src/mailer/templates/mailer/base.html @@ -49,6 +49,13 @@ btn btn-outline-primary btn-sm" aria-current="page" href="{% url 'mailer:phone' %}"> Send SMS + + Settings +
diff --git a/src/mailer/templates/mailer/index.html b/src/mailer/templates/mailer/index.html index 13fc995..16885b5 100644 --- a/src/mailer/templates/mailer/index.html +++ b/src/mailer/templates/mailer/index.html @@ -4,7 +4,7 @@ {% block content %}
-
+ {% csrf_token %} {{ form.media }} @@ -63,7 +63,7 @@
- {% for error in form.start.errors %} {{ error }} @@ -72,7 +72,7 @@ -
- {% for error in form.stop.errors %} {{ error }} @@ -89,6 +89,14 @@ {{ error }} {% endfor %}
+ +
+ + {{ form.mail_manager }} + {% for error in form.mail_manager.errors %} + {{ error }} + {% endfor %} +
diff --git a/src/mailer/templates/mailer/settings.html b/src/mailer/templates/mailer/settings.html new file mode 100644 index 0000000..4e5d40b --- /dev/null +++ b/src/mailer/templates/mailer/settings.html @@ -0,0 +1,168 @@ +{% extends 'mailer/base.html' %} +{% load mytags %} + +{% block content %} + +
+
+ Create Email Manager + + + {% csrf_token %} +
+
+
+
+ + {{ form.label }} + {% for error in form.label.errors %} + {{ error }} + {% endfor %} +
+
+ + {{ form.mail_manager }} + {% for error in form.mail_manager.errors %} + {{ error }} + {% endfor %} +
+
+
+
+
+ {% for mform in forms %} + + {% endfor %} +
+
+
+ + + + +
+

Managers

+ + + + + + + + + + + + {% for email_manager in email_managers %} + + + + + + + {% endfor %} + + +
#LabelManagerActions
1{{ email_manager.label }}{{ email_manager.get_mail_manager_display }} +
+ + +
+
+
+
+ + {{ managers|json_script:"managers_list" }} + + + + +
+ +{% endblock content %} + + +{% block extra_js %} + + + +{% endblock extra_js %} + \ No newline at end of file diff --git a/src/mailer/templates/mailer/sms.html b/src/mailer/templates/mailer/sms.html index 18a46e2..3b2d846 100644 --- a/src/mailer/templates/mailer/sms.html +++ b/src/mailer/templates/mailer/sms.html @@ -6,7 +6,7 @@
-
+ Send SMS diff --git a/src/mailer/urls.py b/src/mailer/urls.py index 1fddea7..f83b626 100644 --- a/src/mailer/urls.py +++ b/src/mailer/urls.py @@ -4,5 +4,6 @@ app_name = 'mailer' urlpatterns = [ path('', views.Dashboard.as_view(), name='home'), - path('send-sms/', views.SmsDashboard.as_view(), name='phone') + path('send-sms/', views.SmsDashboard.as_view(), name='phone'), + path('settings/', views.Settings.as_view(), name='settings'), ] diff --git a/src/mailer/views.py b/src/mailer/views.py index 46bd0fc..6fc1ef5 100644 --- a/src/mailer/views.py +++ b/src/mailer/views.py @@ -4,35 +4,35 @@ from django.conf import settings from django.contrib import messages -from django.http import HttpRequest -from django.http.response import HttpResponse from django.shortcuts import redirect -from django.views.generic import TemplateView -from messenger.email_manager import ZeptoEmailManager +from django.views.generic import TemplateView, ListView +from mailer.mail_manager import MANAGER_CONFIG +from mailer.models import EmailManager from messenger.sms_manager import SmsManager from messenger.messager import ExcelMessenger from messenger.messsage_manager import HtmlMessageManager from utils.general import count_true_in_iter -from utils.loggers import err_logger, logger # noqa +from utils.loggers import err_logger # noqa -from .forms import MailForm +from .forms import EmailManagerConfigForm, MailForm class Dashboard(TemplateView): """ Email Sender Dashboard """ - template_name = 'mailer/index.html' - message_template = 'mailer/email_templates/template1.html' + + template_name = "mailer/index.html" + message_template = "mailer/email_templates/template1.html" extra_context = { - 'title': 'Email Sender', - 'page': 'home', + "title": "Email Sender", + "page": "home", } form_class = MailForm def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context = super().get_context_data(**kwargs) - context['form'] = MailForm() + context["form"] = MailForm() return context def get_message_context(self, data: dict): @@ -44,9 +44,7 @@ def get_message_context(self, data: dict): :return: message context :rtype: dict """ - return { - 'reply_to': data.get('reply_to') - } + return {"reply_to": data.get("reply_to")} def create_message_manager(self, data: dict): """ @@ -60,7 +58,7 @@ def create_message_manager(self, data: dict): return HtmlMessageManager( template_name=self.message_template, request=self.request, - context=self.get_message_context(data) + context=self.get_message_context(data), ) def create_sender_manager(self, data: dict): @@ -72,15 +70,10 @@ def create_sender_manager(self, data: dict): :return: sender manager :rtype: Email manager """ - sender = data.get('sender') - reply_to = data.get('reply_to') - return ZeptoEmailManager( - api_key=settings.ZEPTOTOKEN, - sender=f"{sender}@{settings.EMAIL_DOMAIN}", - block_send=settings.BLOCK_EMAIL, - debug=settings.DEBUG_EMAIL, - reply_email=reply_to - ) + sender = data.get("sender") + reply_to = data.get("reply_to") + mail_manager = data.get("mail_manager") + return mail_manager.get_email_manager(sender, reply_to) def create_messenger(self, file_path: str, data: dict): """ @@ -93,20 +86,16 @@ def create_messenger(self, file_path: str, data: dict): :return: messenger :rtype: ExcelMessenger """ - start = data.get('start') - stop = data.get('stop') + start = data.get("start") + stop = data.get("stop") messenger = ExcelMessenger( start=start, stop=stop, file_path=file_path, - recipient_field=data.get('email_key') - ) - messenger.set_sender_manager( - self.create_sender_manager(data) - ) - messenger.set_message_manager( - self.create_message_manager(data) + recipient_field=data.get("email_key"), ) + messenger.set_sender_manager(self.create_sender_manager(data)) + messenger.set_message_manager(self.create_message_manager(data)) return messenger def post(self, request, **kwargs): @@ -118,37 +107,39 @@ def post(self, request, **kwargs): if form.is_valid(): obj = form.save() - subject = form.cleaned_data.get('subject') - message = json.loads(form.cleaned_data.get('content')).get('html') - start = form.cleaned_data.get('start') - stop = form.cleaned_data.get('stop') + subject = form.cleaned_data.get("subject") + message = json.loads(form.cleaned_data.get("content")).get("html") + start = form.cleaned_data.get("start") + stop = form.cleaned_data.get("stop") attachments = [] - files = request.FILES.getlist('attachments') + files = request.FILES.getlist("attachments") for file in files: - attachments.append({ - 'filename': file.name, - 'data': file.read(), - "mime_type": file.content_type - }) + attachments.append( + { + "filename": file.name, + "data": file.read(), + "mime_type": file.content_type, + } + ) file_path = obj.file.path messages_to_be_sent = stop - start + 1 completed = False - + try: messenger = self.create_messenger(file_path, form.cleaned_data) sents_fails = messenger.start_process( - subject=subject, - message=message + subject=subject, message=message, attachments=attachments ) sents = count_true_in_iter(sents_fails) fails = messages_to_be_sent - sents - response_message = "{} messages sent, {} messages failed"\ - .format(sents, fails) + response_message = "{} messages sent, {} messages failed".format( + sents, fails + ) messages.success(request, response_message) completed = True @@ -156,13 +147,11 @@ def post(self, request, **kwargs): err_logger.exception(e) messages.warning(request, e) - raise Exception(response_message) obj.delete() os.remove(file_path) if completed: return redirect(self.request.get_full_path()) - print(form.errors) context = self.get_context_data(form=form) return self.render_to_response(context) @@ -171,16 +160,17 @@ class SmsDashboard(Dashboard): """ SMS Sender Dashboard """ - template_name = 'mailer/sms.html' - message_template = 'mailer/sms_templates/template1.html' + + template_name = "mailer/sms.html" + message_template = "mailer/sms_templates/template1.html" extra_context = { - 'title': 'SMS Sender', - 'page': 'phone', + "title": "SMS Sender", + "page": "phone", } def get_message_context(self, data: dict): return { - 'subject': data.get('subject'), + "subject": data.get("subject"), } def create_sender_manager(self, data: dict): @@ -192,7 +182,7 @@ def create_sender_manager(self, data: dict): :return: sender manager :rtype: SmsManager """ - sender = data.get('sender') + sender = data.get("sender") return SmsManager( sid=settings.TWILIO_SID, token=settings.TWILIO_TOKEN, @@ -200,3 +190,51 @@ def create_sender_manager(self, data: dict): block_send=False and settings.BLOCK_EMAIL, debug=settings.DEBUG_EMAIL, ) + + +class Settings(ListView): + """ + Email Sender Settings + """ + + template_name = "mailer/settings.html" + extra_context = { + "title": "Settings", + "page": "settings", + } + model = EmailManager + context_object_name = "email_managers" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = EmailManagerConfigForm() + forms_dict = { + manager: MANAGER_CONFIG[manager]["form"](manager=manager, prefix=manager) + for manager in MANAGER_CONFIG.keys() + } + context["forms"] = list(forms_dict.values()) + context["managers"] = {"managers": list(MANAGER_CONFIG.keys())} + return context + + def post(self, request, **kwargs): + self.object_list = self.get_queryset() + context = self.get_context_data() + form = EmailManagerConfigForm(data=request.POST) + if form.is_valid(): + manager = form.cleaned_data.get("mail_manager") + manager_form = MANAGER_CONFIG[manager]["form"](data=request.POST, prefix=manager, manager=manager) + if manager_form.is_valid(): + config = manager_form.cleaned_data + form.save(config) + messages.success(request, "Manager added successfully") + return redirect("mailer:settings") + else: + messages.error(request, "Invalid form data") + forms_dict = { + manager: MANAGER_CONFIG[manager]["form"](manager=manager, prefix=manager) + for manager in MANAGER_CONFIG.keys() + } + forms_dict[manager] = manager_form + context["forms"] = list(forms_dict.values()) + context["form"] = form + return self.render_to_response(context) diff --git a/src/messenger/email_manager.py b/src/messenger/email_manager.py index 04d88f5..364eb5b 100644 --- a/src/messenger/email_manager.py +++ b/src/messenger/email_manager.py @@ -8,7 +8,7 @@ from django.conf import settings from messenger.smtp import EmailConnection, get_connection from utils.general import bytes_to_base64_str, is_success -from utils.loggers import logger +from utils.loggers import logger, err_logger from .sender_manager import BaseSenderManager @@ -281,6 +281,6 @@ def send( attachments=attachments, ) except Exception as e: - logger.exception(e) + err_logger.exception(e) return False return True diff --git a/src/messenger/smtp.py b/src/messenger/smtp.py index 524c02e..fb935c1 100644 --- a/src/messenger/smtp.py +++ b/src/messenger/smtp.py @@ -5,10 +5,22 @@ from email.mime.multipart import MIMEMultipart from email import encoders from email.mime.base import MIMEBase -import logging +from utils.loggers import err_logger, logger # noqa -class EmailConnection(ContextDecorator): +class SingletonMeta(type): + """ + A metaclass for creating a singleton class. + """ + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + instance = super().__call__(*args, **kwargs) + cls._instances[cls] = instance + return cls._instances[cls] + +class EmailConnection(ContextDecorator, metaclass=SingletonMeta): """ This class is responsible for connecting to the email server and sending the email. @@ -21,9 +33,13 @@ def __init__(self, host: str, port: int, username: str, password: str): self.password = password self.connection = None self.context = ssl.create_default_context() + def __enter__(self): - logging.info("Connecting to the email server") + if self.connection is not None: + logger.info("Connection already exists") + return self + logger.info("Connecting to the email server") if self.port == 465: self.connection = smtplib.SMTP_SSL( self.host, self.port, context=self.context @@ -31,13 +47,13 @@ def __enter__(self): else: self.connection = smtplib.SMTP(self.host, self.port) self.connection.starttls(context=self.context) - logging.info("Logging into the email server") + logger.info("Logging into the email server") self.connection.login(self.username, self.password) - logging.info("Successfully logged in") + logger.info("Successfully logged in") return self def __exit__(self, *exc): - logging.info("Disconnecting from the email server") + logger.info("Disconnecting from the email server") self.connection.quit() def send( @@ -77,6 +93,7 @@ def send( message.attach(MIMEText(html, "html")) if attachments is not None: + logger.info("Adding attachments to the email") for attachment in attachments: part = MIMEBase("application", "octet-stream") part.set_payload(attachment["data"]) @@ -92,15 +109,12 @@ def send( self.connection.sendmail(self.username, recipient, message.as_string()) return True except Exception as e: - logging.error(f"Failed to send email: {e}") + err_logger.error(f"Failed to send email: {e}") + err_logger.exception(e) raise e -connection: EmailConnection = None - - def get_connection(host: str, port: int, username: str, password: str): - global connection - if connection is None: - connection = EmailConnection(host, port, username, password) + connection = EmailConnection(host, port, username, password) + connection.__enter__() return connection From ab1325532519ac360dc3d66d0775e66a3455ac27 Mon Sep 17 00:00:00 2001 From: Ayanwola Ayomide <77179231+devvspaces@users.noreply.github.com> Date: Sun, 5 Jan 2025 15:06:40 +0100 Subject: [PATCH 10/10] update --- .dockerignore | 2 + Dockerfile | 15 ++++++++ compose.yml | 16 ++++++++ requirements.txt | 74 +++++++++++++++++++++---------------- src/.env.dev | 5 +++ src/config/settings/base.py | 2 +- src/config/settings/dev.py | 13 +------ src/entrypoint.sh | 5 +++ src/manage.py | 2 +- 9 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 compose.yml create mode 100644 src/.env.dev create mode 100644 src/entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03d05b5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +venv +.vscode \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..797fc7a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.11 + +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN chmod +x ./src/entrypoint.sh + +EXPOSE 8000 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..721dc7d --- /dev/null +++ b/compose.yml @@ -0,0 +1,16 @@ +version: '3.5' +services: + thunder: + build: . + ports: + - "7000:8000" + container_name: thunder + volumes: + - db-data:/app/src + env_file: + - ./src/.env + working_dir: /app/src + entrypoint: ./entrypoint.sh + +volumes: + db-data: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2d9cbc7..7c66be8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,32 +1,42 @@ -asgiref==3.5.2 -atomicwrites==1.4.0 -attrs==21.4.0 -certifi==2022.6.15 -charset-normalizer==2.1.0 -colorama==0.4.5 -coverage==6.4.1 -Django==4.0.5 -execnet==1.9.0 -idna==3.3 -iniconfig==1.1.1 -numpy==1.23.0 -packaging==21.3 -pandas==1.4.3 -pluggy==1.0.0 -py==1.11.0 -pyparsing==3.0.9 -pytest==7.1.2 -pytest-cov==3.0.0 -pytest-django==4.5.2 -pytest-forked==1.4.0 -pytest-lazy-fixture==0.6.3 -pytest-xdist==2.5.0 -python-dateutil==2.8.2 -python-decouple==3.6 -pytz==2022.1 -requests==2.28.1 -six==1.16.0 -sqlparse==0.4.2 -tomli==2.0.1 -tzdata==2022.1 -urllib3==1.26.9 +aiohappyeyeballs==2.3.5 +aiohttp==3.10.3 +aiohttp-retry==2.8.3 +aiosignal==1.3.1 +asgiref==3.5.2 +atomicwrites==1.4.0 +attrs==21.4.0 +certifi==2022.6.15 +charset-normalizer==2.1.0 +colorama==0.4.5 +coverage==6.4.1 +Django==4.0.5 +django-quill-editor==0.1.40 +execnet==1.9.0 +frozenlist==1.4.1 +idna==3.3 +iniconfig==1.1.1 +multidict==6.0.5 +numpy==1.23.0 +packaging==21.3 +pandas==1.4.3 +pluggy==1.0.0 +py==1.11.0 +PyJWT==2.9.0 +pyparsing==3.0.9 +pytest==7.1.2 +pytest-cov==3.0.0 +pytest-django==4.5.2 +pytest-forked==1.4.0 +pytest-lazy-fixture==0.6.3 +pytest-xdist==2.5.0 +python-dateutil==2.8.2 +python-decouple==3.6 +pytz==2022.1 +requests==2.28.1 +six==1.16.0 +sqlparse==0.4.2 +tomli==2.0.1 +twilio==9.2.3 +tzdata==2022.1 +urllib3==1.26.9 +yarl==1.9.4 diff --git a/src/.env.dev b/src/.env.dev new file mode 100644 index 0000000..8358473 --- /dev/null +++ b/src/.env.dev @@ -0,0 +1,5 @@ +SECRET_KEY='test' +DEBUG=True +BLOCK_EMAIL=False +DEBUG_EMAIL=False +DJANGO_SETTINGS_MODULE='config.settings.dev' diff --git a/src/config/settings/base.py b/src/config/settings/base.py index bd4564f..1b90b9e 100644 --- a/src/config/settings/base.py +++ b/src/config/settings/base.py @@ -4,7 +4,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent -SECRET_KEY = config('SECRET_KEY', default='test') +SECRET_KEY = config('SECRET_KEY', default='django-insecure-ddd1') DEBUG = config('DEBUG', default=True, cast=bool) diff --git a/src/config/settings/dev.py b/src/config/settings/dev.py index 4bc0d03..ce292eb 100644 --- a/src/config/settings/dev.py +++ b/src/config/settings/dev.py @@ -4,14 +4,5 @@ ALLOWED_HOSTS = ["*"] -SEND_GRID = config('SEND_GRID', default='test') -ZEPTOTOKEN = config('ZEPTOTOKEN', default='test') -EMAIL_DOMAIN = config('EMAIL_DOMAIN', default='test') -BLOCK_EMAIL = config('BLOCK_EMAIL', default=True, cast=bool) -DEBUG_EMAIL = config('DEBUG_EMAIL', default=True, cast=bool) - - -EMAIL_USER = config('EMAIL_USER', default='test') -EMAIL_PASSWORD = config('EMAIL_PASSWORD', default='test') -EMAIL_HOST = config('EMAIL_HOST', default='test') -EMAIL_PORT = config('EMAIL_PORT', default='test', cast=int) +BLOCK_EMAIL = config('BLOCK_EMAIL', default=False, cast=bool) +DEBUG_EMAIL = config('DEBUG_EMAIL', default=False, cast=bool) diff --git a/src/entrypoint.sh b/src/entrypoint.sh new file mode 100644 index 0000000..a62159e --- /dev/null +++ b/src/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +python manage.py migrate +python manage.py collectstatic --no-input +python manage.py runserver 0.0.0.0:8000 diff --git a/src/manage.py b/src/manage.py index c797c8b..0c33ae9 100644 --- a/src/manage.py +++ b/src/manage.py @@ -9,7 +9,7 @@ def main(): """Run administrative tasks.""" os.environ.setdefault( - 'DJANGO_SETTINGS_MODULE', "config.settings.dev") + 'DJANGO_SETTINGS_MODULE', config('DJANGO_SETTINGS_MODULE')) try: from django.core.management import execute_from_command_line except ImportError as exc: