Skip to content

Commit dc93bc9

Browse files
committed
Merge branch 'develop' of https://github.com/PythonFreeCourse/calendar into horoscope
2 parents f238b90 + a908f4d commit dc93bc9

38 files changed

+998
-159
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
dev.db
22
test.db
33
config.py
4-
.vscode/settings.json
54

65
# Byte-compiled / optimized / DLL files
76
__pycache__/
@@ -142,4 +141,5 @@ dmypy.json
142141
.vscode/
143142
app/.vscode/
144143

145-
app/routers/stam
144+
# PyCharm
145+
.idea

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PyLander
22

3-
<p align="center">
4-
<img title="Apache-2.0" src="https://img.shields.io/github/license/PythonFreeCourse/calendar.svg">
3+
<p style="text-align:center">
4+
<img title="Apache-2.0" alt="License Apache-2.0 icon" src="https://img.shields.io/github/license/PythonFreeCourse/calendar.svg">
55
</p>
66

77
👋 Welcome to Open Source Calendar built with Python. 🐍
@@ -39,5 +39,11 @@ cp app/config.py.example app/configuration.py
3939
# Edit the variables' values.
4040
uvicorn app.main:app --reload
4141
```
42+
### Running tests
43+
```shell
44+
python -m pytest --cov-report term-missing --cov=app tests
45+
```
46+
4247
## Contributing
4348
View [contributing guidelines](https://github.com/PythonFreeCourse/calendar/blob/master/CONTRIBUTING.md).
49+

app/config.py.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import os
22

33
from fastapi_mail import ConnectionConfig
44
from pydantic import BaseSettings
5+
from starlette.templating import Jinja2Templates
56

67

78
class Settings(BaseSettings):
@@ -45,6 +46,15 @@ email_conf = ConnectionConfig(
4546
USE_CREDENTIALS=True,
4647
)
4748

49+
templates = Jinja2Templates(directory=os.path.join("app", "templates"))
50+
51+
# application name
52+
CALENDAR_SITE_NAME = "Calendar"
53+
# link to the home page of the application
54+
CALENDAR_HOME_PAGE = "calendar.pythonic.guru"
55+
# link to the application registration page
56+
CALENDAR_REGISTRATION_PAGE = r"calendar.pythonic.guru/registration"
57+
4858
# import
4959
MAX_FILE_SIZE_MB = 5 # 5MB
5060
VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files.
@@ -54,6 +64,7 @@ EVENT_HEADER_NOT_EMPTY = 1 # 1- for not empty, 0- for empty.
5464
EVENT_HEADER_LIMIT = 50 # Max characters for event header.
5565
EVENT_CONTENT_LIMIT = 500 # Max characters for event characters.
5666
MAX_EVENTS_START_DATE = 10 # Max Events with the same start date.
67+
5768
# PATHS
5869
STATIC_ABS_PATH = os.path.abspath("static")
5970

app/database/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from datetime import datetime
22

3-
from app.config import PSQL_ENVIRONMENT
4-
from app.database.database import Base
53
from sqlalchemy import (DDL, Boolean, Column, DateTime, ForeignKey, Index,
64
Integer, String, event)
75
from sqlalchemy.dialects.postgresql import TSVECTOR
86
from sqlalchemy.orm import relationship
97

8+
from app.config import PSQL_ENVIRONMENT
9+
from app.database.database import Base
10+
1011

1112
class UserEvent(Base):
1213
__tablename__ = "user_event"
@@ -30,6 +31,7 @@ class User(Base):
3031
email = Column(String, unique=True, nullable=False)
3132
password = Column(String, nullable=False)
3233
full_name = Column(String)
34+
language = Column(String)
3335
description = Column(String, default="Happy new user!")
3436
avatar = Column(String, default="profile.png")
3537
telegram_id = Column(String, unique=True)

app/internal/email.py

Lines changed: 148 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
from app.config import email_conf
1+
import os
2+
from typing import List, Optional
3+
4+
5+
from app.config import (email_conf, templates,
6+
CALENDAR_SITE_NAME, CALENDAR_HOME_PAGE,
7+
CALENDAR_REGISTRATION_PAGE)
28
from app.database.models import Event, User
3-
from fastapi import BackgroundTasks
9+
from fastapi import BackgroundTasks, UploadFile
410
from fastapi_mail import FastMail, MessageSchema
11+
from pydantic import EmailStr
12+
from pydantic.errors import EmailError
513
from sqlalchemy.orm.session import Session
614

715
mail = FastMail(email_conf)
@@ -31,10 +39,142 @@ def send(
3139
User.id == user_to_send).first()
3240
if not user_to_send or not event_used:
3341
return False
34-
message = MessageSchema(
35-
subject=f"{title} {event_used.title}",
36-
recipients={"email": [user_to_send.email]}.get("email"),
37-
body=f"begins at:{event_used.start} : {event_used.content}",
38-
)
39-
background_tasks.add_task(mail.send_message, message)
42+
if not verify_email_pattern(user_to_send.email):
43+
return False
44+
45+
subject = f"{title} {event_used.title}"
46+
recipients = {"email": [user_to_send.email]}.get("email")
47+
body = f"begins at:{event_used.start} : {event_used.content}"
48+
49+
background_tasks.add_task(send_internal,
50+
subject=subject,
51+
recipients=recipients,
52+
body=body)
4053
return True
54+
55+
56+
def send_email_invitation(sender_name: str,
57+
recipient_name: str,
58+
recipient_mail: str,
59+
background_tasks: BackgroundTasks = BackgroundTasks
60+
) -> bool:
61+
"""
62+
This function takes as parameters the sender's name,
63+
the recipient's name and his email address, configuration, and
64+
sends the recipient an invitation to his email address in
65+
the format HTML.
66+
:param sender_name: str, the sender's name
67+
:param recipient_name: str, the recipient's name
68+
:param recipient_mail: str, the recipient's email address
69+
:param background_tasks: (BackgroundTasks): Function from fastapi that lets
70+
you apply tasks in the background.
71+
:return: bool, True if the invitation was successfully
72+
sent to the recipient, and False if the entered
73+
email address is incorrect.
74+
"""
75+
if not verify_email_pattern(recipient_mail):
76+
return False
77+
78+
if not recipient_name:
79+
return False
80+
81+
if not sender_name:
82+
return False
83+
84+
template = templates.get_template("invite_mail.html")
85+
html = template.render(recipient=recipient_name, sender=sender_name,
86+
site_name=CALENDAR_SITE_NAME,
87+
registration_link=CALENDAR_REGISTRATION_PAGE,
88+
home_link=CALENDAR_HOME_PAGE,
89+
addr_to=recipient_mail)
90+
91+
subject = "Invitation"
92+
recipients = [recipient_mail]
93+
body = html
94+
subtype = "html"
95+
96+
background_tasks.add_task(send_internal,
97+
subject=subject,
98+
recipients=recipients,
99+
body=body,
100+
subtype=subtype)
101+
return True
102+
103+
104+
def send_email_file(file_path: str,
105+
recipient_mail: str,
106+
background_tasks: BackgroundTasks = BackgroundTasks):
107+
"""
108+
his function takes as parameters the file's path,
109+
the recipient's email address, configuration, and
110+
sends the recipient an file to his email address.
111+
:param file_path: str, the file's path
112+
:param recipient_mail: str, the recipient's email address
113+
:param background_tasks: (BackgroundTasks): Function from fastapi that lets
114+
you apply tasks in the background.
115+
:return: bool, True if the file was successfully
116+
sent to the recipient, and False if the entered
117+
email address is incorrect or file does not exist.
118+
"""
119+
if not verify_email_pattern(recipient_mail):
120+
return False
121+
122+
if not os.path.exists(file_path):
123+
return False
124+
125+
subject = "File"
126+
recipients = [recipient_mail]
127+
body = "file"
128+
file_attachments = [file_path]
129+
130+
background_tasks.add_task(send_internal,
131+
subject=subject,
132+
recipients=recipients,
133+
body=body,
134+
file_attachments=file_attachments)
135+
return True
136+
137+
138+
async def send_internal(subject: str,
139+
recipients: List[str],
140+
body: str,
141+
subtype: Optional[str] = None,
142+
file_attachments: Optional[List[str]] = None):
143+
if file_attachments is None:
144+
file_attachments = []
145+
146+
message = MessageSchema(
147+
subject=subject,
148+
recipients=[EmailStr(recipient) for recipient in recipients],
149+
body=body,
150+
subtype=subtype,
151+
attachments=[UploadFile(file_attachment)
152+
for file_attachment in file_attachments])
153+
154+
return await send_internal_internal(message)
155+
156+
157+
async def send_internal_internal(msg: MessageSchema):
158+
"""
159+
This function receives message and
160+
configuration as parameters and sends the message.
161+
:param msg: MessageSchema, message
162+
:return: None
163+
"""
164+
await mail.send_message(msg)
165+
166+
167+
def verify_email_pattern(email: str) -> bool:
168+
"""
169+
This function checks the correctness
170+
of the entered email address
171+
:param email: str, the entered email address
172+
:return: bool,
173+
True if the entered email address is correct,
174+
False if the entered email address is incorrect.
175+
"""
176+
try:
177+
EmailStr.validate(email)
178+
return True
179+
except EmailError:
180+
return False

app/internal/search.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from typing import List
22

3+
from sqlalchemy.exc import SQLAlchemyError
4+
35
from app.database.database import SessionLocal
46
from app.database.models import Event
5-
from sqlalchemy.exc import SQLAlchemyError
67

78

89
def get_stripped_keywords(keywords: str) -> str:

app/internal/translation.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from typing import Optional
2+
3+
from iso639 import languages
4+
from textblob import TextBlob, download_corpora
5+
from textblob.exceptions import NotTranslated
6+
7+
from app.database.database import SessionLocal
8+
from loguru import logger
9+
10+
from app.routers.user import get_users
11+
12+
download_corpora.download_all()
13+
14+
15+
def translate_text(text: str,
16+
target_lang: str,
17+
original_lang: Optional[str] = None
18+
) -> str:
19+
"""
20+
Translate text to the target language
21+
optionally given the original language
22+
"""
23+
if not text.strip():
24+
return ""
25+
if original_lang is None:
26+
original_lang = _detect_text_language(text)
27+
else:
28+
original_lang = _lang_full_to_short(original_lang)
29+
30+
if original_lang == _lang_full_to_short(target_lang):
31+
return text
32+
33+
try:
34+
return str(TextBlob(text).translate(
35+
from_lang=original_lang,
36+
to=_lang_full_to_short(target_lang)))
37+
except NotTranslated:
38+
return text
39+
40+
41+
def _detect_text_language(text: str) -> str:
42+
"""
43+
Gets some text and returns the language it is in
44+
Uses external API
45+
"""
46+
return str(TextBlob(text).detect_language())
47+
48+
49+
def _get_user_language(user_id: int, session: SessionLocal) -> str:
50+
"""
51+
Gets a user-id and returns the language he speaks
52+
Uses the DB"""
53+
try:
54+
user = get_users(session, id=user_id)[0]
55+
except IndexError:
56+
logger.exception(
57+
"User was not found in the database."
58+
)
59+
return ""
60+
else:
61+
return user.language
62+
63+
64+
def translate_text_for_user(text: str,
65+
session: SessionLocal,
66+
user_id: int) -> str:
67+
"""
68+
Gets a text and a user-id and returns the text,
69+
translated to the language the user speaks
70+
"""
71+
target_lang = _get_user_language(user_id, session)
72+
if not target_lang:
73+
return text
74+
return translate_text(text, target_lang)
75+
76+
77+
def _lang_full_to_short(full_lang: str) -> str:
78+
"""
79+
Gets the full language name and
80+
converts it to a two-letter language name
81+
"""
82+
return languages.get(name=full_lang.capitalize()).alpha2

app/internal/weather_forecast.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import datetime
2-
import frozendict
32
import functools
3+
4+
import frozendict
45
import requests
56

67
from app import config
78

8-
99
# This feature requires an API KEY
1010
# get yours free @ visual-crossing-weather.p.rapidapi.com
1111

app/routers/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
import nltk
22

3-
43
nltk.download('punkt')

app/routers/dayview.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from app.database.models import Event, User
1010
from app.dependencies import TEMPLATES_PATH
1111

12-
1312
templates = Jinja2Templates(directory=TEMPLATES_PATH)
1413

1514

0 commit comments

Comments
 (0)