Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new page that subscribes user to a subscription when visited #318

Merged
1 commit merged into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions api/tests/routes/api/v1/preference_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import json
from datetime import datetime

import pytest
from yelp_beans.models import MeetingSubscription
from yelp_beans.models import Rule
from yelp_beans.models import SubscriptionDateTime
from yelp_beans.models import User
from yelp_beans.models import UserSubscriptionPreferences
from yelp_beans.routes.api.v1 import preferences
from yelp_beans.routes.api.v1.preferences import preferences_api
from yelp_beans.routes.api.v1.preferences import preferences_api_post
Expand Down Expand Up @@ -51,3 +58,213 @@ def test_preference_api_post(monkeypatch, app, database, fake_user):

assert response == "OK"
assert fake_user.subscription_preferences == []


def test_subscribe_api_post_no_user(client, session):
sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
subscription = MeetingSubscription(datetime=[sub_time])
session.add(subscription)
session.commit()
resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={})
assert resp.status_code == 400


def test_subscribe_api_post_user_cant_subscribe(client, session):
user = User(first_name="tester", last_name="user", email="[email protected]", meta_data={"email": "[email protected]"})
session.add(user)

sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
rule = Rule(name="email", value="[email protected]")
subscription = MeetingSubscription(
datetime=[sub_time],
rule_logic="all",
user_rules=[rule],
)
session.add(subscription)
session.commit()
resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email})
assert resp.status_code == 403


def test_subscribe_api_post_user_has_existing(client, session):
sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
subscription = MeetingSubscription(
timezone="America/Los_Angeles",
datetime=[sub_time],
title="Test",
size=2,
office="tester",
location="test place",
user_rules=[],
default_auto_opt_in=False,
)
session.add(subscription)

# Setting auto_opt_in to true because subscription has default of false
preference = UserSubscriptionPreferences(subscription=subscription, preference=sub_time, auto_opt_in=True)
user = User(
first_name="tester",
last_name="user",
email="[email protected]",
meta_data={"email": "[email protected]"},
subscription_preferences=[preference],
)
session.add(user)
session.commit()

resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email})
assert resp.status_code == 200
assert resp.json == {
"subscription": {
"id": subscription.id,
"default_auto_opt_in": False,
"location": "test place",
"name": "Test",
"office": "tester",
"rule_logic": None,
"rules": [],
"size": 2,
# Keeping the first of the preferences
"time_slots": [{"day": "thursday", "hour": 6, "minute": 0}],
"timezone": "America/Los_Angeles",
},
"time_slot": {
"day": "thursday",
"hour": 6,
"minute": 0,
},
"new_preference": False,
}

new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one()
assert new_preference == preference


@pytest.mark.parametrize("default_auto_opt_in", (True, False))
def test_subscribe_api_post_user_new_subscription(client, session, default_auto_opt_in):
sub_time = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
subscription = MeetingSubscription(
timezone="America/Los_Angeles",
datetime=[sub_time],
title="Test",
size=2,
office="tester",
location="test place",
user_rules=[],
default_auto_opt_in=default_auto_opt_in,
)
session.add(subscription)

user = User(
first_name="tester",
last_name="user",
email="[email protected]",
meta_data={"email": "[email protected]"},
subscription_preferences=[],
)
session.add(user)
session.commit()
resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email})
assert resp.status_code == 200
assert resp.json == {
"subscription": {
"id": subscription.id,
"default_auto_opt_in": default_auto_opt_in,
"location": "test place",
"name": "Test",
"office": "tester",
"rule_logic": None,
"rules": [],
"size": 2,
# Keeping the first of the preferences
"time_slots": [{"day": "thursday", "hour": 6, "minute": 0}],
"timezone": "America/Los_Angeles",
},
"time_slot": {
"day": "thursday",
"hour": 6,
"minute": 0,
},
"new_preference": True,
}

new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one()
assert new_preference.auto_opt_in == default_auto_opt_in
assert new_preference.subscription == subscription
assert new_preference.preference == sub_time


def test_subscribe_api_post_user_no_popular(client, session):
sub_time_1 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
sub_time_2 = SubscriptionDateTime(datetime=datetime(2017, 8, 20, 13, 0))
subscription = MeetingSubscription(
timezone="America/Los_Angeles",
datetime=[sub_time_1, sub_time_2],
title="Test",
size=2,
office="tester",
location="test place",
user_rules=[],
default_auto_opt_in=True,
)
session.add(subscription)

user = User(
first_name="tester",
last_name="user",
email="[email protected]",
meta_data={"email": "[email protected]"},
subscription_preferences=[],
)
session.add(user)
session.commit()

resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email})
assert resp.status_code == 200
assert resp.json["time_slot"] == {
"day": "thursday",
"hour": 6,
"minute": 0,
}

new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one()
assert new_preference.preference == sub_time_1


def test_subscribe_api_post_user_pick_popular(client, session):
sub_time_1 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 13, 0))
sub_time_2 = SubscriptionDateTime(datetime=datetime(2017, 7, 20, 18, 0))
subscription = MeetingSubscription(
timezone="America/Los_Angeles",
datetime=[sub_time_1, sub_time_2],
title="Test",
size=2,
office="tester",
location="test place",
user_rules=[],
default_auto_opt_in=True,
)
session.add(subscription)
# Make a fake preference, so second time slot is more popular
preference = UserSubscriptionPreferences(subscription=subscription, preference=sub_time_2, auto_opt_in=True, user_id=200)
session.add(preference)

user = User(
first_name="tester",
last_name="user",
email="[email protected]",
meta_data={"email": "[email protected]"},
subscription_preferences=[],
)
session.add(user)
session.commit()
resp = client.post(f"/v1/user/preferences/subscribe/{subscription.id}", json={"email": user.email})
assert resp.status_code == 200
assert resp.json["time_slot"] == {
"day": "thursday",
"hour": 11,
"minute": 0,
}

new_preference = session.query(UserSubscriptionPreferences).filter(UserSubscriptionPreferences.user_id == user.id).one()
assert new_preference.preference == sub_time_2
23 changes: 22 additions & 1 deletion api/yelp_beans/logic/subscription.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from datetime import datetime
from datetime import timedelta
from typing import Any

from database import db
from pytz import timezone
from pytz import utc
from sqlalchemy import func

from yelp_beans.models import MeetingSpec
from yelp_beans.models import MeetingSubscription
from yelp_beans.models import Rule
from yelp_beans.models import User
from yelp_beans.models import UserSubscriptionPreferences


def filter_subscriptions_by_user_data(subscriptions, user):
Expand All @@ -20,7 +25,9 @@ def filter_subscriptions_by_user_data(subscriptions, user):
return approved_subscriptions


def apply_rules(user, subscription, subscription_rules):
def apply_rules(
user: User, subscription: MeetingSubscription | dict[str, Any], subscription_rules: list[Rule]
) -> MeetingSubscription | dict[str, Any] | None:
"""
Apply logic to rules set for each subscription. In a way this authorizes who can
see the subscription. Rules can be applied in two ways: All rules must apply and
Expand Down Expand Up @@ -133,3 +140,17 @@ def store_specs_from_subscription(subscription, week_start, specs):
db.session.add_all(specs)
db.session.commit()
return specs


def get_subscriber_counts(subscription_id: int) -> dict[int, int]:
counts_query = (
db.select(UserSubscriptionPreferences.preference_id, func.count("*"))
.filter(UserSubscriptionPreferences.subscription_id == subscription_id)
.group_by(UserSubscriptionPreferences.preference_id)
)
counts = db.session.execute(counts_query).all()
return dict(counts)


def get_subscription(subscription_id: int) -> MeetingSubscription:
return MeetingSubscription.query.filter(MeetingSubscription.id == subscription_id).one()
9 changes: 8 additions & 1 deletion api/yelp_beans/logic/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import logging
from typing import NotRequired
from typing import TypedDict

from database import db

Expand Down Expand Up @@ -182,7 +184,12 @@ def remove_preferences(user, updated_preferences, subscription_id):
return removed


def add_preferences(user, updated_preferences, subscription_id):
class PreferenceOptions(TypedDict):
active: NotRequired[bool]
auto_opt_in: NotRequired[bool]


def add_preferences(user: User, updated_preferences: dict[int, PreferenceOptions], subscription_id: int) -> set[int]:
"""
Parameters
----------
Expand Down
46 changes: 46 additions & 0 deletions api/yelp_beans/routes/api/v1/preferences.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import logging
from typing import Any

from flask import Blueprint
from flask import jsonify
from flask import request

from yelp_beans.logic.subscription import apply_rules
from yelp_beans.logic.subscription import filter_subscriptions_by_user_data
from yelp_beans.logic.subscription import get_subscriber_counts
from yelp_beans.logic.subscription import get_subscription
from yelp_beans.logic.subscription import merge_subscriptions_with_preferences
from yelp_beans.logic.user import add_preferences
from yelp_beans.logic.user import get_user
from yelp_beans.logic.user import remove_preferences
from yelp_beans.routes.api.v1.types import Subscription
from yelp_beans.routes.api.v1.types import TimeSlot

preferences_blueprint = Blueprint("preferences", __name__)

Expand Down Expand Up @@ -51,3 +57,43 @@ def preferences_api_post(subscription_id: int) -> str:
logging.info(added)

return "OK"


@preferences_blueprint.route("/subscribe/<int:subscription_id>", methods=["POST"])
def subscribe_api_post(subscription_id: int) -> dict[str, Any]:
data = request.json
user = get_user(data.get("email"))
if not user:
resp = jsonify({"msg": f"A user doesn't exist with the email of \"{data.get('email')}\""})
resp.status_code = 400
return resp

subscription = get_subscription(subscription_id)
approved = apply_rules(user, subscription, subscription.user_rules)
if not approved:
resp = jsonify({"msg": "You are not eligible for this subscription"})
resp.status_code = 403
return resp

datetime_to_subscriber_counts = get_subscriber_counts(subscription_id)
if datetime_to_subscriber_counts:
best_datetime_id, _ = max(datetime_to_subscriber_counts.items(), key=lambda row: row[1])
else:
# No most popular time slot, so just pick the first one
best_datetime_id = subscription.datetime[0].id

existing_matching_prefs = [pref for pref in user.subscription_preferences if pref.subscription_id == subscription_id]

if existing_matching_prefs:
new_preference = False
else:
add_preferences(user, {best_datetime_id: {"active": True}}, subscription_id)
new_preference = True

# get_subscriber_counts return this datetime id, so it should always exist in subscription.datetime
datetime = next(rule for rule in subscription.datetime if rule.id == best_datetime_id)
return {
"subscription": Subscription.from_sqlalchemy(subscription).model_dump(mode="json"),
"time_slot": TimeSlot.from_sqlalchemy(datetime, subscription.timezone).model_dump(mode="json"),
"new_preference": new_preference,
}
Loading
Loading