diff --git a/app/database/models.py b/app/database/models.py index c8e38b44..e1258970 100644 --- a/app/database/models.py +++ b/app/database/models.py @@ -38,8 +38,8 @@ class UserFeature(Base): __tablename__ = "user_feature" id = Column(Integer, primary_key=True, index=True) - feature_id = Column('feature_id', Integer, ForeignKey('features.id')) - user_id = Column('user_id', Integer, ForeignKey('users.id')) + feature_id = Column("feature_id", Integer, ForeignKey("features.id")) + user_id = Column("user_id", Integer, ForeignKey("users.id")) is_enable = Column(Boolean, default=False) @@ -60,6 +60,7 @@ class User(Base): privacy = Column(String, default="Private", nullable=False) is_manager = Column(Boolean, default=False) language_id = Column(Integer, ForeignKey("languages.id")) + availability = Column(Boolean, default=True, nullable=False) target_weight = Column(Float, nullable=True) owned_events = relationship( @@ -124,6 +125,7 @@ class Event(Base): color = Column(String, nullable=True) all_day = Column(Boolean, default=False) invitees = Column(String) + is_public = Column(Boolean, default=False) privacy = Column(String, default=PrivacyKinds.Public.name, nullable=False) emotion = Column(String, nullable=True) image = Column(String, nullable=True) diff --git a/app/internal/email.py b/app/internal/email.py index d75e1e09..71f30ad8 100644 --- a/app/internal/email.py +++ b/app/internal/email.py @@ -1,7 +1,7 @@ import os from typing import List, Optional -from fastapi import BackgroundTasks, UploadFile +from fastapi import BackgroundTasks, Depends, UploadFile from fastapi_mail import FastMail, MessageSchema from pydantic import EmailStr from pydantic.errors import EmailError @@ -14,8 +14,9 @@ DOMAIN, email_conf, ) -from app.database.models import Event, User +from app.database.models import Event, User, UserEvent from app.dependencies import templates +from app.internal.security.dependencies import current_user from app.internal.security.schema import ForgotPassword mail = FastMail(email_conf) @@ -26,6 +27,7 @@ def send( event_used: int, user_to_send: int, title: str, + content: str = "", background_tasks: BackgroundTasks = BackgroundTasks, ) -> bool: """This function is being used to send emails in the background. @@ -57,11 +59,52 @@ def send( send_internal, subject=subject, recipients=recipients, - body=body, + body=body + content, ) return True +def send_email_to_event_participants( + session: Session, + event_id: int, + title: str, + content: str, + user_logged: User = Depends(current_user), +) -> int: + """This function sends emails to a mailing list of all event participants. + it uses the function send above to do this and avoid double codes.. + Args: + session(Session): The session to redirect to the database. + event_id (int): Id number of the event that is used. + title (str): Title of the email that is being sent. + content (str): body of email sent. + Returns: + int: Returns the number of emails sent + (number of valid emails in event's participants) + """ + event_owner = session.query(Event.owner).filter(id == event_id).first() + if event_owner != user_logged: + return 0 + # makes sure only event owner can send an email via this func. + mailing_list = ( + session.query(User.id, User.email) + .join(UserEvent, User.id == UserEvent.user_id) + .filter(event_id == event_id) + .all() + ) + valid_mailing_list = list(filter(verify_email_pattern, mailing_list.email)) + if not valid_mailing_list: + return 0 + # making sure app doesn't crash if emails are invalid + + event = session.query(Event).get(event_id) + subject = f"{event.title}: {title}" + for r in valid_mailing_list: + send(session, event, r.id, subject, content) + # sends the send email function parameters to send on the mailing list + return len(valid_mailing_list) + + def send_email_invitation( sender_name: str, recipient_name: str, diff --git a/app/routers/email.py b/app/routers/email.py index 908c35a2..13b0bad5 100644 --- a/app/routers/email.py +++ b/app/routers/email.py @@ -17,17 +17,20 @@ @router.post("/send") async def send( - db: Session = Depends(get_db), - send_to: str = "/", - title: str = Form(...), - event_used: str = Form(...), - user_to_send: str = Form(...), - background_tasks: BackgroundTasks = BackgroundTasks + db: Session = Depends(get_db), + send_to: str = "/", + title: str = Form(...), + event_used: str = Form(...), + user_to_send: str = Form(...), + background_tasks: BackgroundTasks = BackgroundTasks, ) -> RedirectResponse: if not internal_send( - title=title, event_used=event_used, - user_to_send=user_to_send, - background_tasks=background_tasks, session=db): + title=title, + event_used=event_used, + user_to_send=user_to_send, + background_tasks=background_tasks, + session=db, + ): raise HTTPException(status_code=404, detail="Couldn't send the email!") return RedirectResponse(send_to, status_code=303) @@ -44,8 +47,10 @@ class InvitationParams(BaseModel): @router.post("/invitation/") -def send_invitation(invitation: InvitationParams, - background_task: BackgroundTasks): +def send_invitation( + invitation: InvitationParams, + background_task: BackgroundTasks, +): """ This function sends the recipient an invitation to his email address in the format HTML. @@ -60,12 +65,14 @@ def send_invitation(invitation: InvitationParams, except EmailError: raise HTTPException( status_code=422, - detail=INVALID_EMAIL_ADDRESS_ERROR_MESSAGE) + detail=INVALID_EMAIL_ADDRESS_ERROR_MESSAGE, + ) if not send_email_invitation( - sender_name=invitation.sender_name, - recipient_name=invitation.recipient_name, - recipient_mail=invitation.recipient_mail, - background_tasks=background_task): + sender_name=invitation.sender_name, + recipient_name=invitation.recipient_name, + recipient_mail=invitation.recipient_mail, + background_tasks=background_task, + ): raise HTTPException(status_code=422, detail="Couldn't send the email!") return RedirectResponse(invitation.send_to, status_code=303) diff --git a/app/routers/event.py b/app/routers/event.py index edc28a2f..c7196ca6 100644 --- a/app/routers/event.py +++ b/app/routers/event.py @@ -41,7 +41,7 @@ ) from app.internal.privacy import PrivacyKinds from app.internal.security.dependencies import current_user -from app.internal.utils import create_model, get_current_user +from app.internal.utils import create_model, get_current_user, save from app.routers.categories import get_user_categories IMAGE_HEIGHT = 200 @@ -488,6 +488,7 @@ def create_event( longitude: Optional[str] = None, color: Optional[str] = None, invitees: List[str] = None, + is_public: bool = False, category_id: Optional[int] = None, availability: bool = True, is_google_event: bool = False, @@ -515,6 +516,7 @@ def create_event( color=color, emotion=get_emotion(title, content), invitees=invitees_concatenated, + is_public=is_public, all_day=all_day, category_id=category_id, shared_list=shared_list, @@ -617,6 +619,24 @@ def add_new_event(values: dict, db: Session) -> Optional[Event]: return None +def add_user_to_event(session: Session, user_id: int, event_id: int): + user_already_connected = ( + session.query(UserEvent) + .filter_by(event_id=event_id, user_id=user_id) + .all() + ) + + # if the user has a connection to the event, + # the function will recognize the duplicate and return false. + + if user_already_connected: + return False + # if user is not registered to the event, the system will add him + association = UserEvent(user_id=user_id, event_id=event_id) + save(session, association) + return True + + def extract_shared_list_from_data( event_info: ImmutableMultiDict, db: Session, diff --git a/tests/conftest.py b/tests/conftest.py index 9cc2cf02..2b942665 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import calendar +from datetime import datetime import nest_asyncio import pytest @@ -7,6 +8,7 @@ from app.config import PSQL_ENVIRONMENT from app.database.models import Base +from app.routers.event import create_event pytest_plugins = [ "tests.fixtures.user_fixture", @@ -92,4 +94,26 @@ def Calendar(): return calendar.Calendar(0) +# the following fixtures are meant to replace user1, user2,user3 +# fixtures inside test_user. they do the same but those have more indicative +# name, so after the feature freeze ill make sure to replace them completely. +# did not delete user1, user2 and user3 since some people +# use them in testing now. + + +@pytest.fixture +def event_example(session, event_owning_user): + data = { + "title": "test event title", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/87538459r6", + "content": "content", + "owner_id": event_owning_user.id, + } + + event = create_event(session, **data) + return event + + nest_asyncio.apply() diff --git a/tests/fixtures/user_fixture.py b/tests/fixtures/user_fixture.py index e2a7ad26..143f5f06 100644 --- a/tests/fixtures/user_fixture.py +++ b/tests/fixtures/user_fixture.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import Generator import pytest @@ -6,7 +7,8 @@ from app.database.models import User from app.database.schemas import UserCreate from app.internal.utils import create_model, delete_instance -from app.routers.register import create_user +from app.routers.event import create_event +from app.routers.register import _create_user, create_user @pytest.fixture @@ -38,3 +40,55 @@ def sender(session: Session) -> Generator[User, None, None]: ) yield mock_user delete_instance(session, mock_user) + + +@pytest.fixture +def no_event_user(session): + """a user made for testing who doesn't own any event.""" + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new2_test.email@gmail.com", + language_id="english", + ) + + return user + + +@pytest.fixture +def event_owning_user(session): + """a user made for testing who already owns an event.""" + user = _create_user( + session=session, + username="new_test_username2", + password="new_test_password2", + email="new_test_love231.email@gmail.com", + language_id="english", + ) + + data = { + "title": "event_owning_user event", + "start": datetime.strptime("2021-05-05 14:59", "%Y-%m-%d %H:%M"), + "end": datetime.strptime("2021-05-05 15:01", "%Y-%m-%d %H:%M"), + "location": "https://us02web.zoom.us/j/875384596", + "content": "content", + "owner_id": user.id, + } + + create_event(session, **data) + + return user + + +@pytest.fixture +def user1(session): + """another user made for testing""" + user = _create_user( + session=session, + username="user2user2", + password="verynicepass", + email="trulyyours1.email@gmail.com", + language_id="english", + ) + return user diff --git a/tests/test_email.py b/tests/test_email.py index 10022beb..6e4279dd 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -2,15 +2,16 @@ from fastapi import BackgroundTasks, status from sqlalchemy.orm import Session -from app.database.models import User +from app.database.models import User, UserEvent from app.internal.email import ( mail, send, send_email_file, send_email_invitation, + send_email_to_event_participants, verify_email_pattern, ) -from app.internal.utils import create_model, delete_instance +from app.internal.utils import create_model, delete_instance, save def test_email_send(client, user, event, smtpd): @@ -296,3 +297,54 @@ def test_send(session, bad_user, event): ) def test_verify_email_pattern(email): assert not verify_email_pattern(email) + + +def test_sending_mailing_list_with_no_user( + session, + no_event_user, + event_owning_user, + user1, + event_example, +): + """this test assures a wrong user won't be able to use the mailing list""" + association = UserEvent( + user_id=no_event_user.id, + event_id=event_example.id, + ) + save(session, association) + + association2 = UserEvent(user_id=user1.id, event_id=event_example.id) + save(session, association2) + + num_emails_send = send_email_to_event_participants( + session, + event_example.id, + "this mail example", + "booboo", + ) + assert num_emails_send == 0 + + +def test_sending_mailing_list_from_event_owner( + session, + no_event_user, + event_owning_user, + user1, + event_example, + client, + security_test_client, +): + """this test assures mailing list is sent successfuly from + the event owner. assiciations were created already at the test above.""" + # logged_user_data = {'username': event_owning_user.username, + # 'password': event_owning_user.password} + pass + # res = security_test_client.post( + # security_test_client.app.url_path_for('login'), + # data=logged_user_data) + # TODO: log in event_owning_user to assure successful mailing list send + + # if res.status_code == HTTP_302_FOUND: + # num_emails_send = send_email_to_event_participants( + # session, event_example.id, 'this mail example', 'booboo') + # assert num_emails_send == 2 diff --git a/tests/test_event.py b/tests/test_event.py index 597a5b7b..813d7dac 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -18,7 +18,8 @@ from app.internal.utils import delete_instance from app.main import app from app.routers import event as evt -from app.routers.event import event_to_show +from app.routers.event import add_new_event, add_user_to_event, event_to_show +from app.routers.register import _create_user CORRECT_EVENT_FORM_DATA = { "title": "test title", @@ -162,6 +163,42 @@ LOGIN_DATA = {"username": "correct_user", "password": "correct_password"} +@pytest.fixture +def new_event(session, new_user): + event = add_new_event(TestApp.event_test_data, session) + return event + + +@pytest.fixture +def new_user(session): + user = _create_user( + session=session, + username="new_test_username", + password="new_test_password", + email="new_test.email@gmail.com", + language_id="english", + ) + + return user + + +def test_joining_public_event(session, new_event, new_user): + """test in order to make sure user is added the first time + he asks to join event, yet won't join the same user twice""" + first_join = add_user_to_event( + session, + event_id=new_event.id, + user_id=new_user.id, + ) + assert first_join + second_join = add_user_to_event( + session, + event_id=new_event.id, + user_id=new_user.id, + ) + assert not second_join + + def test_get_events(event_test_client, session, event): response = event_test_client.get("/event/") assert response.ok diff --git a/tests/test_user.py b/tests/test_user.py index b2dc545c..47d147ed 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -68,10 +68,10 @@ def event1(session, user2): return event -def test_disabling_no_event_user(session, user1): +def test_disabling_no_event_user(session, no_event_user): # users without any future event can disable themselves - disable(session, user1.id) - assert user1.disabled + disable(session, no_event_user.id) + assert no_event_user.disabled future_events = list( session.query(Event.id) .join(UserEvent) @@ -110,10 +110,10 @@ def test_disabling_user_participating_event(session, user1, event1): session.delete(deleted_user_event_connection) -def test_disabling_event_owning_user(session, user2): +def test_disabling_event_owning_user(session, event_owning_user): # making sure user owning events can't disable itself - disable(session, user2.id) - assert not user2.disabled + disable(session, event_owning_user.id) + assert not event_owning_user.disabled class TestUser: