diff --git a/reqs/reqlinux.txt b/reqs/reqlinux.txt index 7f3ad5b..967f521 100644 --- a/reqs/reqlinux.txt +++ b/reqs/reqlinux.txt @@ -6,6 +6,12 @@ # asgiref==3.6.0 # via django +boto3==1.28.77 + # via -r reqs\reqlinux.in +botocore==1.31.78 + # via + # boto3 + # s3transfer certifi==2023.5.7 # via # requests @@ -59,6 +65,10 @@ django-widget-tweaks==1.4.12 # via -r reqs\reqlinux.in idna==3.4 # via requests +jmespath==1.0.1 + # via + # boto3 + # botocore oauthlib==3.2.2 # via requests-oauthlib pillow==9.5.0 @@ -73,6 +83,8 @@ pycparser==2.21 # via cffi pyjwt[crypto]==2.7.0 # via django-allauth +python-dateutil==2.8.2 + # via botocore python-magic==0.4.27 # via -r reqs\reqlinux.in python3-openid==3.2.0 @@ -86,8 +98,12 @@ requests==2.30.0 # requests-oauthlib requests-oauthlib==1.3.1 # via django-allauth +s3transfer==0.7.0 + # via boto3 sentry-sdk==1.2 # via -r reqs\reqlinux.in +six==1.16.0 + # via python-dateutil sqlparse==0.4.4 # via django typing-extensions==4.7.1 @@ -98,5 +114,6 @@ unidecode==1.3.6 # via -r reqs\reqlinux.in urllib3==2.0.2 # via + # botocore # requests - # sentry-sdk + # sentry-sdk \ No newline at end of file diff --git a/sandbox/conf/base.py b/sandbox/conf/base.py index 8d76124..93453fc 100644 --- a/sandbox/conf/base.py +++ b/sandbox/conf/base.py @@ -133,7 +133,7 @@ ] -LANGUAGE_CODE = "en-us" +LANGUAGE_CODE = "en" LANGUAGES = (("ru", _("Russian")), ("en", _("English")), ("uk", _("Ukrainian"))) USE_I18N = True @@ -167,8 +167,7 @@ EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD") EMAIL_PORT = env("EMAIL_PORT") EMAIL_USE_TLS = True -EMAIL_BACKEND = env("EMAIL_BACKEND") -# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # dev.py +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # dev.py # img upload limits @@ -392,14 +391,14 @@ # AWS -AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") -AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") -AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") +# AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") +# AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") +# AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") -AWS_S3_FILE_OVERWRITE = False -AWS_DEFAULT_ACL = None -DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" +# AWS_S3_FILE_OVERWRITE = False +# AWS_DEFAULT_ACL = None +# DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" -AWS_QUERYSTRING_AUTH = False # key will be not present in url -AWS_S3_MAX_MEMORY_SIZE = 2200000 -AWS_S3_REGION_NAME = "eu-central-1" +# AWS_QUERYSTRING_AUTH = False # key will be not present in url +# AWS_S3_MAX_MEMORY_SIZE = 2200000 +# AWS_S3_REGION_NAME = "eu-central-1" diff --git a/src/contacts/jobs/__init__.py b/src/contacts/jobs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/contacts/jobs/send_news.py b/src/contacts/management/commands/send_news_letter.py similarity index 84% rename from src/contacts/jobs/send_news.py rename to src/contacts/management/commands/send_news_letter.py index b1a427b..856fa23 100644 --- a/src/contacts/jobs/send_news.py +++ b/src/contacts/management/commands/send_news_letter.py @@ -2,9 +2,9 @@ from django.conf import settings from django.core import mail +from django.core.management.base import BaseCommand from django.template.loader import render_to_string from django.utils import timezone -from django_extensions.management.jobs import WeeklyJob from src.contacts.exceptions import * # noqa from src.contacts.models import NewsLetter @@ -12,18 +12,18 @@ from src.profiles.models import Profile -class Job(WeeklyJob): +class Command(BaseCommand): """ users: active status and profile wanted_niews - will get email with news weekly; + will get email with news weekly?; News letter may not contain links to posts TODO: better way to arrange unsubscribe """ - help = "Send news letter" # noqa + help = "Send a newsletter to subscribed users" # noqa - def execute(self): + def handle(self, *args, **options): _date = timezone.localdate() str_date = _date.strftime("%d/%m/%Y") stamp = f"Newsletter {_date:%A}, {_date:%b}. {_date:%d} {str_date}" @@ -31,9 +31,10 @@ def execute(self): profiles = Profile.objects.send_news().select_related("user") letter = NewsLetter.objects.filter(letter_status=1).last() ctx = {"letter": letter, "domain": domain} + if profiles and letter: try: - posts = Post.objects.filter(send_status=1, letter=letter) + posts = Post.objects.get_public().filter(send_status=1, letter=letter) ctx.update({"posts": posts}) for profile in profiles: ctx.update({"uuid": profile.uuid}) @@ -53,6 +54,7 @@ def execute(self): for post in posts: post.send_status = 2 post.save() + self.stdout.write(self.style.SUCCESS("Successfully sent newletter")) except SMTPException as e: print("smth went wrong ", e) diff --git a/src/contacts/tests/test_jobs.py b/src/contacts/tests/test_jobs.py index 937e4b6..7d2b9a8 100644 --- a/src/contacts/tests/test_jobs.py +++ b/src/contacts/tests/test_jobs.py @@ -1,9 +1,11 @@ import time_machine from django.conf import settings +from django.core import mail +from django.core.management import call_command +from django.test import TestCase, override_settings from django.urls import reverse from src.contacts.exceptions import * # noqa -from src.contacts.jobs.send_news import Job as SendMailJob from src.contacts.models import NewsLetter from src.posts.models.post_model import Post from src.posts.tests.factories import PostFactory @@ -12,57 +14,60 @@ from .factories import NewsLetterFactory -class TestSendEmailJob: +@override_settings(LANGUAGE_CODE="ru", LANGUAGES=(("ru", "Russian"),)) +class TestSendEmailJob(TestCase): @time_machine.travel("2023-07-17 00:00 +0000") - def test_send_news_with_posts_links(self, mailoutbox): + def test_send_news_with_posts_links(self): """ active user (can) get news letter via email with corresp links to posts; if sending OK: letter and related posts - change their status + change their status; + email (html)text contains a link to unsubscribe """ subject = "Newsletter Monday, Jul. 17 17/07/2023" profile = ProfileFactory(want_news=True) domain = settings.ABSOLUTE_URL_BASE letter = NewsLetterFactory(letter_status=1) - post = PostFactory(send_status=1, letter=letter, title_ru="заголовок") + post = PostFactory(send_status=1, status=2, letter=letter, title_ru="заголовок") post_title = post.title_ru short_url = reverse("contacts:end_news", kwargs={"uuid": profile.uuid}) full_link_unsub = f"{domain}{short_url}" - send_mail_job = SendMailJob() - send_mail_job.execute() + call_command("send_news_letter") - assert len(mailoutbox) == 1 + self.assertEqual(len(mail.outbox), 1) - mail = mailoutbox[0] - html_msg = mail.alternatives[0][0] + sent_mail = mail.outbox[0] - assert mail.to == [profile.user.email] - assert mail.subject == subject + html_msg = sent_mail.alternatives[0][0] - assert letter.text in mail.body - assert letter.text in html_msg - # TODO: change title for a link to post - assert post_title in mail.body - assert post_title in html_msg - # email (html)text contains a link to unsubscribe - assert full_link_unsub in mail.body - assert full_link_unsub in html_msg + self.assertTrue(sent_mail.to == [profile.user.email]) + self.assertTrue(sent_mail.subject == subject) + self.assertTrue(letter.text in sent_mail.body) + self.assertTrue(letter.text in html_msg) + + self.assertIn(post_title, sent_mail.body) + self.assertIn(post_title, html_msg) + self.assertIn(full_link_unsub, sent_mail.body) + self.assertIn(full_link_unsub, html_msg) + + # after sending post_after = Post.objects.filter(send_status=2).last() letter_after = NewsLetter.objects.filter(letter_status=2).last() - assert post.id == post_after.id - assert post_after.send_status == 2 - assert letter.id == letter_after.id - assert letter_after.letter_status == 2 + self.assertEqual(post.id, post_after.id) + self.assertEqual(post_after.send_status, 2) + self.assertEqual(letter.id, letter_after.id) + self.assertEqual(letter_after.letter_status, 2) @time_machine.travel("2023-07-17 00:00 +0000") - def test_send_news_no_posts(self, mailoutbox): + def test_send_news_no_posts(self): """ No posts links in the letter no posts with send_status + email (html)text contains a link to unsubscribe """ subject = "Newsletter Monday, Jul. 17 17/07/2023" profile = ProfileFactory(want_news=True) @@ -73,29 +78,30 @@ def test_send_news_no_posts(self, mailoutbox): short_url = reverse("contacts:end_news", kwargs={"uuid": profile.uuid}) full_link_unsub = f"{domain}{short_url}" - send_mail_job = SendMailJob() - send_mail_job.execute() + call_command("send_news_letter") + + self.assertEqual(len(mail.outbox), 1) - assert len(mailoutbox) == 1 + sent_mail = mail.outbox[0] + html_msg = sent_mail.alternatives[0][0] + sent_mail = mail.outbox[0] + html_msg = sent_mail.alternatives[0][0] - mail = mailoutbox[0] - html_msg = mail.alternatives[0][0] + self.assertTrue(sent_mail.to == [profile.user.email]) + self.assertTrue(sent_mail.subject == subject) - assert mail.to == [profile.user.email] - assert mail.subject == subject + self.assertTrue(letter.text in sent_mail.body) + self.assertTrue(letter.text in html_msg) + self.assertNotIn(post_title, sent_mail.body) + self.assertNotIn(post_title, html_msg) - assert letter.text in mail.body - assert letter.text in html_msg - # TODO: change title for a link to post - assert post_title not in mail.body - assert post_title not in html_msg - # email (html)text contains a link to unsubscribe - assert full_link_unsub in mail.body - assert full_link_unsub in html_msg + self.assertIn(full_link_unsub, sent_mail.body) + self.assertIn(full_link_unsub, html_msg) + # after sending post_after = Post.objects.filter(send_status=2).count() letter_after = NewsLetter.objects.filter(letter_status=2).last() - assert post_after == 0 - assert letter.id == letter_after.id - assert letter_after.letter_status == 2 + self.assertEqual(post_after, 0) + self.assertEqual(letter.id, letter_after.id) + self.assertEqual(letter_after.letter_status, 2) diff --git a/src/contacts/tests/test_mail_fail.py b/src/contacts/tests/test_mail_fail.py index a8b953e..b771646 100644 --- a/src/contacts/tests/test_mail_fail.py +++ b/src/contacts/tests/test_mail_fail.py @@ -2,10 +2,10 @@ from unittest import mock from django.core import mail +from django.core.management import call_command from django.test import TestCase from src.accounts.models import User -from src.contacts.jobs.send_news import Job as SendMailJob from src.profiles.tests.factories.profile_factory import ProfileFactory from ..exceptions import * # noqa @@ -19,9 +19,7 @@ def test_news_not_send(self, mock_fail): letter = NewsLetterFactory(letter_status=1) # noqa mock_fail.side_effect = SMTPException - - send_mail_job = SendMailJob() - send_mail_job.execute() + call_command("send_news_letter") self.assertEqual(len(mail.outbox), 0) self.assertTrue(mock_fail.called) @@ -41,9 +39,10 @@ def test_manager_no_news_inactive_user(self): user.is_active = False user.save() letter = NewsLetterFactory(letter_status=1) # noqa - send_mail_job = SendMailJob() + with self.assertRaises(NewsFansNotFoundException) as e: - send_mail_job.execute() + call_command("send_news_letter") + self.assertEqual(str(e.exception), "No profiles not send news") self.assertEqual(len(mail.outbox), 0) @@ -54,9 +53,9 @@ def test_manager_no_news_fans(self): """ profile = ProfileFactory() # noqa letter = NewsLetterFactory(letter_status=1) # noqa - send_mail_job = SendMailJob() + with self.assertRaises(NewsFansNotFoundException) as e: - send_mail_job.execute() + call_command("send_news_letter") self.assertEqual(str(e.exception), "No profiles not send news") self.assertEqual(len(mail.outbox), 0) @@ -66,9 +65,9 @@ def test_no_letter_no_job(self): if no letter -> no SendMail """ profile = ProfileFactory(want_news=True) # noqa - send_mail_job = SendMailJob() + with self.assertRaises(LetterNotFoundException) as e: - send_mail_job.execute() + call_command("send_news_letter") self.assertEqual(str(e.exception), "No letter to send") - assert len(mail.outbox) == 0 + self.assertEqual(len(mail.outbox), 0) diff --git a/src/posts/admin.py b/src/posts/admin.py index 7f2b912..81dd22a 100644 --- a/src/posts/admin.py +++ b/src/posts/admin.py @@ -52,7 +52,7 @@ class PostAdmin(TranslationAdmin): list_display_links = ["title"] radio_fields = {"status": admin.HORIZONTAL} save_on_top = True - list_filter = ["status", "created_at", SoftDelFilter] + list_filter = ["status", "created_at", SoftDelFilter, "send_status"] # list_filter = ["status", "created_at", "is_deleted"] list_per_page = 15 actions = ("make_posts_published", "set_to_draft", "set_to_review") diff --git a/src/profiles/models.py b/src/profiles/models.py index 856f285..ecea89c 100644 --- a/src/profiles/models.py +++ b/src/profiles/models.py @@ -1,7 +1,5 @@ import uuid -from boto3.session import Session -from django.conf import settings from django.contrib.auth import get_user_model from django.db import models from django.shortcuts import reverse @@ -39,26 +37,10 @@ def get_absolute_url(self): def __str__(self) -> str: return self.user.username - # def delete(self): - # """if profile obj deleted; remove avatar file""" - # self.avatar.delete() - # super().delete() - # def delete(self, *args, **kwargs): - # session = Session (settings.AWS_ACCESS_KEY_ID, settings.AWS_SECRET_ACCESS_KEY) - # s3_resource = session.resource('s3') - # s3_bucket = s3_resource.Bucket(settings.AWS_STORAGE_BUCKET_NAME) - - # file_path = f"avatar/{self.id}" + str(self.avatar) - # print("path is ",file_path) - # response = s3_bucket.delete_objects( - # Delete={ - # 'Objects': [ - # { - # 'Key': file_path - # } - # ] - # }) - # super().delete(*args, **kwargs) + def delete(self): + """if profile obj deleted; remove avatar file""" + self.avatar.delete() + super().delete() class ProfileChart(Profile): diff --git a/src/static/css/styles.css b/src/static/css/styles.css index 9d51826..f18dd76 100644 --- a/src/static/css/styles.css +++ b/src/static/css/styles.css @@ -25,6 +25,28 @@ body{ /* width added new 27 april 2023 */ width: 100%; } +/* ######################### +scroll to top button +######################### */ +#toTop { + /* display: none; */ /*default */ + position: fixed; + bottom: 20px; + right: 30px; + z-index: 99; + border: none; + outline: none; + background-color:rgb(165, 46, 6); + color: white; + cursor: pointer; + padding: 15px; + border-radius: 10px; + font-size: 18px; +} + +#toTop:hover { + background-color: #555; +} @media screen and (min-width: 600px){ body{ padding-top:150px; diff --git a/src/static/js/utils.js b/src/static/js/utils.js index 32db025..daf0abb 100644 --- a/src/static/js/utils.js +++ b/src/static/js/utils.js @@ -86,6 +86,26 @@ function fileToDataUri(file) { }); } +// button to the topFunction//Get the button +let toTop = document.getElementById("toTop"); + +// When the user scrolls down 20px from the top of the document, show the button +window.onscroll = function() {scrollFunction()}; + +function scrollFunction() { + if (document.body.scrollTop > 200 || document.documentElement.scrollTop > 200) { + toTop.style.display = "block"; + } else { + toTop.style.display = "none"; + } +} + +// When the user clicks on the button, scroll to the top of the document +function topFunction() { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; +} + diff --git a/src/templates/contacts/emails/letter.html b/src/templates/contacts/emails/letter.html index 15131eb..da3342e 100644 --- a/src/templates/contacts/emails/letter.html +++ b/src/templates/contacts/emails/letter.html @@ -5,28 +5,24 @@

Greetings from MedSandbox

-

Here is some fresh content on our site

{% if letter %} -

Letter title: {{letter.title}}

-

Letter text: {{ letter.text|linebreaksbr }}

-

Here is a link to read a new article: - {% if posts %} +

{{letter.title}}

+

{{ letter.text|linebreaksbr }}

+

Please follow the link to read more + {% if posts %} {% for post in posts %} - Post title: {{post.title_ru}} - {% comment %} - Some post

- {% endcomment %} + {{post.title}}

{% endfor%} {% else %} -

No posts

+

No links to posts

{% endif %} {% endif %}

Best regards,

Admin


- Unsubscribe + Unsubscribe from news letters
- + \ No newline at end of file diff --git a/src/templates/posts/post_list.html b/src/templates/posts/post_list.html index 8b09a6c..d8b9851 100644 --- a/src/templates/posts/post_list.html +++ b/src/templates/posts/post_list.html @@ -39,8 +39,8 @@ {% show_archive %} {% show_tags %} - + {% endblock%} {% block js %}