Skip to content

Commit

Permalink
Consistent service interfaces (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelkornblum authored Dec 27, 2024
1 parent a505944 commit 377a77c
Show file tree
Hide file tree
Showing 20 changed files with 657 additions and 471 deletions.
29 changes: 19 additions & 10 deletions core/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.contrib.auth import get_user_model

from profiles import services as profile_services
from user import services as user_services


Expand All @@ -11,13 +12,21 @@
User = get_user_model()


def get_or_create_user(id: str, *args, **kwargs) -> tuple[User, bool]:
return user_services.get_or_create_user(id, *args, **kwargs)


def get_user_by_id(id: str) -> User:
return user_services.get_user_by_sso_id(id)


# TODO:
# - Add create_profile, get_profile etc. in this service
def create_user(
id: str,
first_name: str,
last_name: str,
emails: list[dict],
) -> User:
"""
Entrypoint for new user creation. Triggers the creation of User record,
then the relevant Profile record as well as a combined Profile.
"""
user = user_services.create(sso_email_id=id)
profile_services.create_from_sso(
id,
first_name,
last_name,
emails,
)
return user
38 changes: 14 additions & 24 deletions core/tests/test_core_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,30 @@
from django.test import TestCase

from core import services as core_services
from user.exceptions import UserExists


class TestCoreService(TestCase):
User = get_user_model()

@pytest.mark.django_db
def test_core_get_or_create_user(self):
user_details = {
"is_active": True,
}
@pytest.mark.skip()
def test_create_user(self):
# User is created
user, created = core_services.get_or_create_user(
user = core_services.create_user(
id="[email protected]",
**user_details,
first_name="Billy",
last_name="Joel",
emails=[{"address": "[email protected]"}],
)
self.assertTrue(created)
self.assertEqual(user.sso_email_id, "[email protected]")
self.assertEqual(user.is_active, True)

# User already exists
existing_user, is_created = core_services.get_or_create_user(
id="[email protected]",
**user_details,
)
self.assertFalse(is_created)

@pytest.mark.django_db
def test_core_get_user_by_id(self):

test_user = self.User.objects.create_user(
sso_email_id="test_user",
is_active=True,
)

user = core_services.get_user_by_id(test_user.sso_email_id)
self.assertEqual(user.sso_email_id, "test_user")
self.assertEqual(user.is_active, True)
with self.assertRaises(UserExists) as ex:
existing_user = core_services.create_user(
id="[email protected]",
first_name="Billy",
last_name="Joel",
emails=[{"address": "[email protected]"}],
)
21 changes: 19 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions profiles/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class ProfileIsArchived(Exception):
def __init__(self, message):
self.message = message


class ProfileIsNotArchived(Exception):
def __init__(self, message):
self.message = message


class ProfileExists(Exception):
def __init__(self, message):
self.message = message
6 changes: 4 additions & 2 deletions profiles/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from user.models import User


TYPES = (("work", "Work"), ("contact", "Contact"))
EMAIL_TYPE_WORK = "work"
EMAIL_TYPE_CONTACT = "contact"
EMAIL_TYPES = ((EMAIL_TYPE_WORK, "Work"), (EMAIL_TYPE_CONTACT, "Contact"))


class AbstractHistoricalModel(models.Model):
Expand Down Expand Up @@ -62,7 +64,7 @@ class StaffSSOProfileEmail(AbstractHistoricalModel):
"StaffSSOProfile", on_delete=models.CASCADE, related_name="emails"
)
email = models.ForeignKey(Email, on_delete=models.CASCADE)
type = models.CharField(max_length=50, choices=TYPES)
type = models.CharField(max_length=50, choices=EMAIL_TYPES)
preferred = models.BooleanField(default=False)

class Meta:
Expand Down
27 changes: 27 additions & 0 deletions profiles/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This is the entrypoint service that coordinates between the sub-services
# If in doubt about what to use, you should probably be using this

from profiles.models import Email
from profiles.services import combined, staff_sso


def generate_combined_profile_fields(sso_user_id: str):
"""
Figures out which info we have in specific profiles and runs through the
field hierarchy per data type to generate the values for the combined.
create method
"""
raise NotImplementedError()


def create_from_sso(
sso_user_id: str,
first_name: str,
last_name: str,
emails: list[dict],
):
"""A central function to create all relevant profile details"""
# staff_sso.create()
# payload = generate_combined_profile_values()
# combined.create(sso_user_id, **payload)
raise NotImplementedError()
86 changes: 86 additions & 0 deletions profiles/services/combined.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import Optional

from profiles.exceptions import ProfileExists, ProfileIsArchived, ProfileIsNotArchived
from profiles.models import Profile


###############################################################
# Base data methods
###############################################################


#### ID is required - no getting around it ####


def get_by_id(sso_email_id: str) -> Profile:
# NB we're not raising more specific exceptions here because we're optimising this for speed
return Profile.objects.get(sso_email_id=sso_email_id, is_active=True)


def create(
sso_email_id: str,
first_name: str,
last_name: str,
emails: list[str],
preferred_email: Optional[str],
) -> Profile:
try:
get_by_id(sso_email_id)
except Profile.DoesNotExist:
return Profile.objects.create(
sso_email_id=sso_email_id,
first_name=first_name,
last_name=last_name,
emails=emails,
preferred_email=preferred_email,
)
raise ProfileExists("Profile has been previously created")


#### Standard profile-object methods ####


def update(
profile: Profile,
first_name: Optional[str],
last_name: Optional[str],
preferred_email: Optional[str],
emails: Optional[list[str]],
) -> None:
update_fields = []
if first_name is not None:
update_fields.append("first_name")
profile.first_name = first_name
if last_name is not None:
update_fields.append("last_name")
profile.last_name = last_name
if preferred_email is not None:
update_fields.append("preferred_email")
profile.preferred_email = preferred_email
if emails is not None:
update_fields.append("emails")
profile.emails = emails
profile.save(update_fields=update_fields)


def archive(profile: Profile) -> None:
"""Soft-delete a profile"""
if not profile.is_active:
raise ProfileIsArchived("Profile is already archived")

profile.is_active = False
profile.save(update_fields=("is_active",))


def unarchive(profile: Profile) -> None:
"""Restore a soft-deleted profile."""
if profile.is_active:
raise ProfileIsNotArchived("Profile is not archived")

profile.is_active = True
profile.save(update_fields=("is_active",))


def delete_from_database(profile: Profile) -> None:
"""Really delete a Profile. Only to be used in data cleaning (i.e. non-standard) operations"""
profile.delete()
59 changes: 0 additions & 59 deletions profiles/services/profile.py

This file was deleted.

Loading

0 comments on commit 377a77c

Please sign in to comment.