diff --git a/api/urls.py b/api/urls.py index 121cfdc..01b8621 100644 --- a/api/urls.py +++ b/api/urls.py @@ -18,7 +18,6 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("user/", include("apps.core.urls")), path("app/", include("apps.core.urls_core")), path("member/", include("apps.core.urls_member")), path("mentorship/", include("apps.mentorship.urls")), @@ -28,5 +27,6 @@ path("company-profile/", include("apps.company.urls_company")), path("event/", include("apps.event.urls")), path("api/", include("apps.core.urls_internal")), - path("auth/", include("apps.core.urls_auth")), + path('api/core/', include('apps.core.urls_new')), + path('api/member/', include('apps.member.urls')), ] diff --git a/apps/core/apps.py b/apps/core/apps.py index 8a906a6..ce4552f 100644 --- a/apps/core/apps.py +++ b/apps/core/apps.py @@ -5,5 +5,3 @@ class CoreConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.core" - def ready(self): - import apps.core.signals diff --git a/apps/core/auth_backends.py b/apps/core/auth_backends.py new file mode 100644 index 0000000..19efad7 --- /dev/null +++ b/apps/core/auth_backends.py @@ -0,0 +1,16 @@ +# In a new file, e.g., core/auth_backends.py +from django.contrib.auth import get_user_model +from django.contrib.auth.backends import ModelBackend + + +class EmailBackend(ModelBackend): + def authenticate(self, request, email=None, password=None, **kwargs): + UserModel = get_user_model() + try: + user = UserModel.objects.get(email=email) + except UserModel.DoesNotExist: + return None + else: + if user.check_password(password): + return user + return None diff --git a/apps/core/serializers.py b/apps/core/serializers.py deleted file mode 100644 index d109797..0000000 --- a/apps/core/serializers.py +++ /dev/null @@ -1,196 +0,0 @@ -from django.contrib.auth import authenticate -from django.utils.translation import gettext_lazy as _ - -from rest_framework import serializers, validators -from rest_framework.authtoken.serializers import AuthTokenSerializer - -from apps.company.models import CompanyProfile, Roles, Department -from apps.company.serializers import RoleSerializer, SkillSerializer -from apps.core.models import UserProfile, CustomUser, CommunityNeeds -from apps.member.models import MemberProfile - - -class CustomAuthTokenSerializer(AuthTokenSerializer): - email = serializers.EmailField() - password = serializers.CharField( - style={"input_type": "password"}, trim_whitespace=True - ) - - def validate(self, attrs): - email = attrs.get("email") - password = attrs.get("password") - - if email and password: - user = authenticate( - request=self.context.get("request"), username=email, password=password - ) - - # The authenticate call simply returns None for is_active=False - # users. (Assuming the default ModelBackend authentication - # backend.) - if not user: - msg = _("Unable to log in with provided credentials.") - raise serializers.ValidationError(msg, code="authorization") - else: - msg = _('Must include "email" and "password".') - raise serializers.ValidationError(msg, code="authorization") - - attrs["user"] = user - return attrs - - -class RegisterSerializer(serializers.ModelSerializer): - class Meta: - model = CustomUser - fields = ("password", "email", "first_name", "last_name") - - extra_kwargs = { - "password": {"write_only": True}, - "email": { - "required": True, - "allow_blank": False, - "validators": [ - validators.UniqueValidator( - CustomUser.objects.all(), - "A user with that email already exists", - ) - ], - }, - } - - def create(self, validated_data): - password = validated_data.get("password") - email = validated_data.get("email") - first_name = validated_data.get("first_name") - last_name = validated_data.get("last_name") - - user = CustomUser.objects.create_user( - email=email, password=password, first_name=first_name, last_name=last_name - ) - return user - - -class UpdateCustomUserSerializer(serializers.ModelSerializer): - class Meta: - model = CustomUser - fields = ( - "is_recruiter", - "is_member", - "is_mentor", - "is_mentee", - "is_mentor_profile_active", - "is_mentor_training_complete", - "is_mentor_profile_approved", - "is_mentor_application_submitted", - "is_talent_source_beta", - "is_speaker", - "is_volunteer", - "is_team", - "is_community_recruiter", - "is_company_account", - "is_partnership", - ) - - -class CommunityNeedsSerializer(serializers.ModelSerializer): - class Meta: - model = CommunityNeeds - fields = '__all__' - - -class UserProfileSerializer(serializers.ModelSerializer): - tbc_program_interest = CommunityNeedsSerializer(many=True, read_only=True) - - class Meta: - model = UserProfile - fields = "__all__" - - -class UpdateProfileAccountDetailsSerializer(serializers.ModelSerializer): - first_name = serializers.CharField(source="user.first_name") - last_name = serializers.CharField(source="user.last_name") - email = serializers.EmailField(source="user.email") - postal_code = serializers.CharField() - location = serializers.CharField() - state = serializers.CharField() - city = serializers.CharField() - - class Meta: - model = MemberProfile - fields = ["first_name", "last_name", "email", "postal_code", "location", "state", "city"] - - def update(self, instance, validated_data): - # Extracting user related data - user_data = validated_data.pop("user", {}) - instance.postal_code = validated_data.get("postal_code", instance.postal_code) - instance.location = validated_data.get("location", instance.location) - instance.state = validated_data.get("state", instance.state) - instance.city = validated_data.get("city", instance.city) - instance.save() - # Updating user instance related fields - user_instance = instance.user - member_profile = user_instance.userprofile - user_instance.first_name = user_data.get("first_name", user_instance.first_name) - user_instance.last_name = user_data.get("last_name", user_instance.last_name) - user_instance.email = user_data.get("email", user_instance.email) - member_profile.location = validated_data.get("location", instance.location) - member_profile.state = validated_data.get("state", instance.state) - member_profile.city = validated_data.get("city", instance.city) - - user_instance.save() - instance.save() - - return instance - - -class CompanyProfileSerializer(serializers.ModelSerializer): - current_employees = serializers.PrimaryKeyRelatedField( - queryset=CustomUser.objects.all(), many=True, required=False - ) - company_name = serializers.CharField(required=False, allow_blank=True) - company_url = serializers.URLField(required=False, allow_blank=True) - - class Meta: - model = CompanyProfile - fields = ["id", "company_name", "company_url", "current_employees"] - - def create(self, validated_data): - user = self.context["request"].user # get the user from the request context - company_name = validated_data.get("company_name", None) - company_url = validated_data.get("company_url", None) - - # Only create a new company if both company_name and company_url are provided. - if not company_name or not company_url: - raise serializers.ValidationError( - "Both company_name and company_url must be provided for new companies." - ) - - company = CompanyProfile.objects.create(**validated_data) - company.current_employees.add(user) - company.save() - - return company - - -class TalentProfileRoleSerializer(serializers.ModelSerializer): - role = serializers.PrimaryKeyRelatedField(queryset=Roles.objects.all(), many=True) - - class Meta: - model = MemberProfile - fields = ["role"] - - -class DepartmentSerializer(serializers.ModelSerializer): - class Meta: - model = Department - fields = ("id", "name") - - -class TalentProfileSerializer(serializers.ModelSerializer): - role = RoleSerializer(many=True, read_only=True) - skills = SkillSerializer(many=True, read_only=True) - department = DepartmentSerializer(many=True, read_only=True) - - class Meta: - model = MemberProfile - fields = "__all__" diff --git a/apps/core/serializers/__init__.py b/apps/core/serializers/__init__.py new file mode 100644 index 0000000..9e238d6 --- /dev/null +++ b/apps/core/serializers/__init__.py @@ -0,0 +1,5 @@ +from .user_serializers import * +from .profile_serializers import * +from .talent_serializers import * +from .company_serializers import * +from .misc_serializers import * diff --git a/apps/core/serializers/company_serializers.py b/apps/core/serializers/company_serializers.py new file mode 100644 index 0000000..75a35a4 --- /dev/null +++ b/apps/core/serializers/company_serializers.py @@ -0,0 +1,27 @@ +# TODO | CODE CLEAN UP: SHOULD BE MOVED TO COMPANY.SERIALIZERS + +from rest_framework import serializers +from apps.company.models import CompanyProfile +from apps.core.models import CustomUser + + +class CompanyProfileSerializer(serializers.ModelSerializer): + """ + Serializer for CompanyProfile model. + """ + current_employees = serializers.PrimaryKeyRelatedField(queryset=CustomUser.objects.all(), many=True, required=False) + + class Meta: + model = CompanyProfile + fields = ["id", "company_name", "company_url", "current_employees"] + extra_kwargs = { + 'company_name': {'required': False, 'allow_blank': True}, + 'company_url': {'required': False, 'allow_blank': True}, + } + + def create(self, validated_data): + if not validated_data.get("company_name") or not validated_data.get("company_url"): + raise serializers.ValidationError("Both company_name and company_url must be provided for new companies.") + company = CompanyProfile.objects.create(**validated_data) + company.current_employees.add(self.context["request"].user) + return company diff --git a/apps/core/serializers/misc_serializers.py b/apps/core/serializers/misc_serializers.py new file mode 100644 index 0000000..6023ba5 --- /dev/null +++ b/apps/core/serializers/misc_serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers +from apps.core.models import CommunityNeeds + + +class CommunityNeedsSerializer(serializers.ModelSerializer): + """ + Serializer for CommunityNeeds model. + """ + + class Meta: + model = CommunityNeeds + fields = '__all__' + + +class GenericBreakdownSerializer(serializers.Serializer): + """ + Generic serializer for breakdown data. + """ + name = serializers.CharField(read_only=True) + user_count = serializers.IntegerField() diff --git a/apps/core/serializers/profile_serializers.py b/apps/core/serializers/profile_serializers.py new file mode 100644 index 0000000..201f43f --- /dev/null +++ b/apps/core/serializers/profile_serializers.py @@ -0,0 +1,103 @@ +from rest_framework import serializers +from apps.core.models import UserProfile +from apps.core.serializers.misc_serializers import CommunityNeedsSerializer +from apps.member.models import MemberProfile +from utils.urls_utils import prepend_https_if_not_empty + + +class BaseUserProfileSerializer(serializers.ModelSerializer): + """ + Base serializer for UserProfile model. + """ + identity_pronouns = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') + + class Meta: + model = UserProfile + fields = "__all__" + + def to_representation(self, instance): + ret = super().to_representation(instance) + hidden_fields = [ + ("identity_sexuality", "is_identity_sexuality_displayed"), + ("identity_gender", "is_identity_gender_displayed"), + ("identity_ethic", "is_identity_ethic_displayed"), + ("identity_pronouns", "is_pronouns_displayed"), + ("disability", "is_disability_displayed"), + ("care_giver", "is_care_giver_displayed"), + ("veteran_status", "is_veteran_status_displayed"), + ] + for field, display_field in hidden_fields: + if not getattr(instance, display_field): + ret.pop(field, None) + return ret + + +class CustomURLField(serializers.URLField): + def to_internal_value(self, data): + """ + Convert the input value to a valid URL format if necessary. + """ + data = prepend_https_if_not_empty(data) + return super().to_internal_value(data) + + +class UserProfileSerializer(BaseUserProfileSerializer): + """ + Serializer for UserProfile model with additional tbc_program_interest field. + """ + tbc_program_interest = serializers.SerializerMethodField() + linkedin = CustomURLField(required=False, allow_blank=True) + github = CustomURLField(required=False, allow_blank=True) + youtube = CustomURLField(required=False, allow_blank=True) + personal = CustomURLField(required=False, allow_blank=True) + + def validate(self, data): + url_fields = ['linkedin', 'github', 'youtube', 'personal'] + for field in url_fields: + if field in data: + data[field] = prepend_https_if_not_empty(data[field]) + return data + + def get_tbc_program_interest(self, obj): + return CommunityNeedsSerializer(obj.tbc_program_interest.all(), many=True).data + + +class ReadOnlyUserProfileSerializer(BaseUserProfileSerializer): + """ + Read-only serializer for UserProfile model with specific fields excluded. + """ + + class Meta(BaseUserProfileSerializer.Meta): + exclude = ['access_token', 'how_connection_made', 'is_terms_agree', 'marketing_events', 'marketing_jobs', + 'marketing_org_updates', 'marketing_monthly_newsletter', 'marketing_identity_based_programing', + 'tbc_program_interest'] + + +class UpdateProfileAccountDetailsSerializer(serializers.ModelSerializer): + """ + Serializer for updating profile account details. + """ + first_name = serializers.CharField(source="user.first_name") + last_name = serializers.CharField(source="user.last_name") + email = serializers.EmailField(source="user.email") + + class Meta: + model = MemberProfile + fields = ["first_name", "last_name", "email", "postal_code", "location", "state", "city"] + + def update(self, instance, validated_data): + user_data = validated_data.pop("user", {}) + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + + user_instance = instance.user + member_profile = user_instance.userprofile + for attr, value in user_data.items(): + setattr(user_instance, attr, value) + setattr(member_profile, attr, value) + user_instance.save() + member_profile.save() + + return instance + diff --git a/apps/core/serializers/talent_serializers.py b/apps/core/serializers/talent_serializers.py new file mode 100644 index 0000000..ce30c8a --- /dev/null +++ b/apps/core/serializers/talent_serializers.py @@ -0,0 +1,70 @@ +from rest_framework import serializers +from apps.member.models import MemberProfile +from .user_serializers import ReadOnlyCustomUserSerializer +from .profile_serializers import ReadOnlyUserProfileSerializer +from ...company.models import CompanyTypes, Department, Roles, Skill, SalaryRange + + +class BaseTalentProfileSerializer(serializers.ModelSerializer): + """ + Base serializer for MemberProfile model. + """ + skills = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') + department = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') + role = serializers.SlugRelatedField(many=True, read_only=True, slug_field='name') + tech_journey_display = serializers.CharField(source='get_tech_journey_display', read_only=True) + + class Meta: + model = MemberProfile + fields = "__all__" + + +class TalentProfileSerializer(serializers.ModelSerializer): + """ + Serializer for MemberProfile model with all fields. + """ + company_types = serializers.PrimaryKeyRelatedField(queryset=CompanyTypes.objects.all(), many=True) + department = serializers.PrimaryKeyRelatedField(queryset=Department.objects.all(), many=True) + role = serializers.PrimaryKeyRelatedField(queryset=Roles.objects.all(), many=True) + skills = serializers.PrimaryKeyRelatedField(queryset=Skill.objects.all(), many=True) + min_compensation = serializers.PrimaryKeyRelatedField(queryset=SalaryRange.objects.all()) + max_compensation = serializers.PrimaryKeyRelatedField(queryset=SalaryRange.objects.all()) + + class Meta: + model = MemberProfile + fields = "__all__" + + +class ReadOnlyTalentProfileSerializer(BaseTalentProfileSerializer): + """ + Read-only serializer for MemberProfile model with specific fields excluded. + """ + + class Meta(BaseTalentProfileSerializer.Meta): + exclude = ["company_types", "created_at", "is_talent_status", "resume"] + + +class FullTalentProfileSerializer(serializers.ModelSerializer): + """ + Comprehensive serializer for MemberProfile model including related data. + """ + user = ReadOnlyCustomUserSerializer(read_only=True) + talent_profile = serializers.SerializerMethodField() + user_profile = serializers.SerializerMethodField() + company_details = serializers.SerializerMethodField() + + class Meta: + model = MemberProfile + fields = "__all__" + + def get_talent_profile(self, obj): + talent_profile = MemberProfile.objects.filter(user=obj.user).first() + return ReadOnlyTalentProfileSerializer(talent_profile).data if talent_profile else None + + def get_user_profile(self, obj): + user_profile = UserProfile.objects.filter(user=obj.user).first() + return ReadOnlyUserProfileSerializer(user_profile).data if user_profile else None + + def get_company_details(self, obj): + from utils.util import get_current_company_data + return get_current_company_data(user=obj.user) diff --git a/apps/core/serializers/user_serializers.py b/apps/core/serializers/user_serializers.py new file mode 100644 index 0000000..607c33c --- /dev/null +++ b/apps/core/serializers/user_serializers.py @@ -0,0 +1,166 @@ +from django.contrib.auth import authenticate, get_user_model +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers +from rest_framework.authtoken.serializers import AuthTokenSerializer +from rest_framework.validators import UniqueValidator + +User = get_user_model() + + +class BaseUserSerializer(serializers.ModelSerializer): + """ + Base serializer for User model with common fields. + """ + + class Meta: + model = User + # fields = ["id", "email", "first_name", "last_name"] + fields = [ + "email", + "first_name", + "last_name", + "is_active", + "is_staff", + "is_recruiter", + "is_member", + "is_talent_choice", + "is_member_onboarding_complete", + "is_slack_invite_sent", + "is_migrated_account", + "is_open_doors", + "is_open_doors_onboarding_complete", + "is_mentor", + "is_mentee", + "is_mentor_profile_active", + "is_mentor_profile_removed", + "is_mentor_training_complete", + "is_mentor_interviewing", + "is_mentor_profile_paused", + "is_mentor_profile_approved", + "is_mentor_application_submitted", + "is_talent_source_beta", + "is_speaker", + "is_volunteer", + "is_team", + "is_community_recruiter", + "is_company_account", + "is_email_confirmation_sent", + "is_email_confirmed", + "is_company_onboarding_complete", + "is_partnership", + "is_company_review_access_active", + "company_review_tokens", + "joined_at", + ] + + +class CustomUserSerializer(BaseUserSerializer): + """ + Serializer for User model with all fields except password. + """ + + class Meta(BaseUserSerializer.Meta): + # exclude = ["password"] + fields = ["id", "email", "first_name", "last_name"] + + +class ReadOnlyCustomUserSerializer(BaseUserSerializer): + """ + Read-only serializer for User model with additional fields. + """ + + class Meta(BaseUserSerializer.Meta): + fields = BaseUserSerializer.Meta.fields + [ + "is_community_recruiter", "is_member", "is_mentee", "is_mentor", + "is_mentor_profile_active", "is_mentor_profile_removed", "is_mentor_training_complete", + "is_mentor_profile_approved", "is_speaker", "is_team", "is_volunteer", "is_mentor_interviewing", + "is_mentor_profile_paused", "joined_at", "is_active" + ] + + +class CustomAuthTokenSerializer(AuthTokenSerializer): + """ + Serializer for user authentication tokens. + """ + email = serializers.EmailField() + password = serializers.CharField(style={"input_type": "password"}, trim_whitespace=True) + + def validate(self, attrs): + email = attrs.get("email") + password = attrs.get("password") + + if email and password: + user = authenticate(request=self.context.get("request"), username=email, email=email, password=password) + if not user: + msg = _("Unable to log in with provided credentials.") + raise serializers.ValidationError(msg, code="authorization") + else: + msg = _('Must include "email" and "password".') + raise serializers.ValidationError(msg, code="authorization") + + attrs["user"] = user + return attrs + + +class RegisterSerializer(serializers.ModelSerializer): + """ + Serializer for user registration. + """ + + class Meta: + model = User + fields = ("password", "email", "first_name", "last_name") + extra_kwargs = { + "password": {"write_only": True}, + "email": { + "required": True, + "allow_blank": False, + "validators": [ + UniqueValidator( + User.objects.all(), + "A user with that email already exists", + ) + ], + }, + } + + def validate_email(self, value): + """ + Ensure the email is always saved in lowercase. + """ + return value.lower() + + def create(self, validated_data): + return User.objects.create_user(**validated_data) + + +class UpdateCustomUserSerializer(serializers.ModelSerializer): + """ + Serializer for updating custom user fields. + """ + + class Meta: + model = User + fields = ( + "is_recruiter", "is_member", "is_mentor", "is_mentee", "is_mentor_profile_active", + "is_mentor_training_complete", "is_mentor_profile_approved", "is_mentor_application_submitted", + "is_talent_source_beta", "is_speaker", "is_volunteer", "is_team", "is_community_recruiter", + "is_company_account", "is_partnership", + ) + + +class UserAccountInfoSerializer(serializers.ModelSerializer): + """ + Serializer to pull account details from user account. + """ + + class Meta: + model = User + fields = [ + "is_staff", "is_recruiter", "is_member", "is_mentor", "is_mentee", + "is_speaker", "is_volunteer", "is_mentor_profile_active", + "is_mentor_training_complete", "is_mentor_profile_approved", + "is_mentor_application_submitted", "is_talent_source_beta", + "is_team", "is_community_recruiter", "is_company_account", + "is_partnership", "is_company_review_access_active" + ] diff --git a/apps/core/serializers_admin.py b/apps/core/serializers_admin.py deleted file mode 100644 index f3839a1..0000000 --- a/apps/core/serializers_admin.py +++ /dev/null @@ -1,6 +0,0 @@ -from rest_framework import serializers - - -class GenericBreakdownSerializer(serializers.Serializer): - name = serializers.CharField(read_only=True) # Assuming each model has a name field - user_count = serializers.IntegerField() diff --git a/apps/core/serializers_member.py b/apps/core/serializers_member.py deleted file mode 100644 index e0e81bf..0000000 --- a/apps/core/serializers_member.py +++ /dev/null @@ -1,165 +0,0 @@ -from rest_framework import serializers -from .models import CustomUser, UserProfile -from .util import get_current_company_data -from ..member.models import MemberProfile - - -class CustomUserSerializer(serializers.ModelSerializer): - class Meta: - model = CustomUser - exclude = ["password"] - - -class ReadOnlyCustomUserSerializer(serializers.ModelSerializer): - class Meta: - model = CustomUser - fields = ["first_name", "last_name", "is_community_recruiter", "is_member", "is_mentee", "is_mentor", - "is_mentor_profile_active", "is_mentor_profile_removed", "is_mentor_training_complete", - "is_mentor_profile_approved", "is_speaker", "is_team", "is_volunteer", "is_mentor_interviewing", - "is_mentor_profile_paused", "joined_at", "is_active", "id"] - - -class UserProfileSerializer(serializers.ModelSerializer): - identity_pronouns = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - - class Meta: - model = UserProfile - fields = "__all__" - - def to_representation(self, instance): - ret = super(UserProfileSerializer, self).to_representation(instance) - - if not instance.is_identity_sexuality_displayed: - ret.pop("identity_sexuality", None) - - if not instance.is_identity_gender_displayed: - ret.pop("identity_gender", None) - - if not instance.is_identity_ethic_displayed: - ret.pop("identity_ethic", None) - - if not instance.is_pronouns_displayed: - ret.pop("identity_pronouns", None) - - if not instance.is_disability_displayed: - ret.pop("disability", None) - - if not instance.is_care_giver_displayed: - ret.pop("care_giver", None) - - if not instance.is_veteran_status_displayed: - ret.pop("veteran_status", None) - - return ret - - -class ReadOnlyUserProfileSerializer(serializers.ModelSerializer): - identity_pronouns = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - - class Meta: - model = UserProfile - exclude = ['access_token', 'how_connection_made', 'is_terms_agree', 'marketing_events', 'marketing_jobs', - 'marketing_org_updates', 'marketing_monthly_newsletter', 'marketing_identity_based_programing', - 'tbc_program_interest'] - - def to_representation(self, instance): - ret = super(ReadOnlyUserProfileSerializer, self).to_representation(instance) - - if not instance.is_identity_sexuality_displayed: - ret.pop("identity_sexuality", None) - - if not instance.is_identity_gender_displayed: - ret.pop("identity_gender", None) - - if not instance.is_identity_ethic_displayed: - ret.pop("identity_ethic", None) - - if not instance.is_pronouns_displayed: - ret.pop("identity_pronouns", None) - - if not instance.is_disability_displayed: - ret.pop("disability", None) - - if not instance.is_care_giver_displayed: - ret.pop("care_giver", None) - - if not instance.is_veteran_status_displayed: - ret.pop("veteran_status", None) - - return ret - - -class TalentProfileSerializer(serializers.ModelSerializer): - skills = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - department = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - role = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - tech_journey_display = serializers.CharField(source='get_tech_journey_display', read_only=True) - - class Meta: - model = MemberProfile - fields = "__all__" - - -class ReadOnlyTalentProfileSerializer(serializers.ModelSerializer): - skills = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - department = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - role = serializers.SlugRelatedField( - many=True, - read_only=True, - slug_field='name' - ) - tech_journey_display = serializers.CharField(source='get_tech_journey_display', read_only=True) - - class Meta: - model = MemberProfile - exclude = ["company_types", "created_at", "is_talent_status", "resume"] - - -class FullTalentProfileSerializer(serializers.ModelSerializer): - user = ReadOnlyCustomUserSerializer(read_only=True) - talent_profile = serializers.SerializerMethodField(read_only=True) - user_profile = serializers.SerializerMethodField(read_only=True) - company_details = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = MemberProfile - fields = "__all__" - - def get_talent_profile(self, obj): - talent_profile = MemberProfile.objects.filter(user=obj.user).first() - return ReadOnlyTalentProfileSerializer(talent_profile).data if talent_profile else None - - def get_user_profile(self, obj): - user_profile = UserProfile.objects.filter(user=obj.user).first() - return ReadOnlyUserProfileSerializer(user_profile).data if user_profile else None - - def get_company_details(selfself, obj): - return get_current_company_data(user=obj.user) diff --git a/apps/core/serializers_open_doors.py b/apps/core/serializers_open_doors.py deleted file mode 100644 index 59202ec..0000000 --- a/apps/core/serializers_open_doors.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework import serializers -from django.core.exceptions import ValidationError - -# Assuming your CustomUser model is the default user model -User = get_user_model() - - -class UserRegistrationSerializer(serializers.ModelSerializer): - password = serializers.CharField(write_only=True) - first_name = serializers.CharField(required=False) # Make first_name optional - last_name = serializers.CharField(required=False) # Make last_name optional - - class Meta: - model = User - fields = ['email', 'first_name', 'last_name', 'password'] - extra_kwargs = { - 'first_name': {'required': False}, # Explicitly marking as optional - 'last_name': {'required': False}, - } - - def validate_email(self, value): - """ - Check that the email provided is valid and not already in use. - """ - User = get_user_model() - if User.objects.filter(email=value).exists(): - raise serializers.ValidationError("A user with that email already exists.") - return value - - def create(self, validated_data): - """ - Create and return a new user, utilizing the custom user manager. - """ - email = validated_data['email'] - password = validated_data['password'] - first_name = validated_data.get('first_name', '') - last_name = validated_data.get('last_name', '') - - # Using the create_user method from your custom user manager. - user = User.objects.create_user( - email=email, - password=password, - first_name=first_name, - last_name=last_name - ) - - # Additional user setup can be done here if necessary - - return user diff --git a/apps/core/signals.py b/apps/core/signals.py deleted file mode 100644 index af3318d..0000000 --- a/apps/core/signals.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from .models import CustomUser, UserProfile -from ..member.models import MemberProfile - - -@receiver(post_save, sender=CustomUser) -def create_user_profile(sender, instance, created, **kwargs): - if created: - UserProfile.objects.create(user=instance) - MemberProfile.objects.create(user=instance) diff --git a/apps/core/urls.py b/apps/core/urls.py deleted file mode 100644 index 41ada1d..0000000 --- a/apps/core/urls.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.urls import path -from knox import views as knox_views -from . import views - - -urlpatterns = [ - path("login/", views.login_api), - path("logout/", knox_views.LogoutView.as_view()), - path("new/", views.create_new_user), - path("new/company", views.create_new_company), - path("details/", views.get_user_data), - path("details/announcement", views.get_announcement), - path("new-member/profile/create", views.create_new_member), - path("details/new-company", views.get_new_company_data), - path("profile/update/account-details", views.update_profile_account_details), - path("profile/update/skills-roles", views.update_profile_skills_roles), - path("profile/update/work-place", views.update_profile_work_place), - path("profile/update/social-accounts", views.update_profile_social_accounts), - path("profile/update/idenity", views.update_profile_identity), - path("profile/update/notifications", views.update_profile_notifications), -] diff --git a/apps/core/urls/admin_urls.py b/apps/core/urls/admin_urls.py new file mode 100644 index 0000000..b88ba67 --- /dev/null +++ b/apps/core/urls/admin_urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from apps.core.views.admin_views import CombinedBreakdownView + +urlpatterns = [ + path('admin/combined-breakdown/', CombinedBreakdownView.as_view(), name='combined-breakdown'), +] \ No newline at end of file diff --git a/apps/core/urls/auth_urls.py b/apps/core/urls/auth_urls.py new file mode 100644 index 0000000..b538092 --- /dev/null +++ b/apps/core/urls/auth_urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from apps.core.views.auth_views import UserPermissionAPIView, LoginView, PasswordResetRequestView, \ + PasswordResetConfirmView, LogoutView + +urlpatterns = [ + path('permissions/', UserPermissionAPIView.as_view(), name='user-permissions'), + path('login/', LoginView.as_view(), name='login'), + path('logout/', LogoutView.as_view(), name='logout'), + path('password-reset/', PasswordResetRequestView.as_view(), name='password-reset-request'), + path('password-reset-confirm///', PasswordResetConfirmView.as_view(), + name='password-reset-confirm'), +] diff --git a/apps/core/urls/company_urls.py b/apps/core/urls/company_urls.py new file mode 100644 index 0000000..a1abc5f --- /dev/null +++ b/apps/core/urls/company_urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from apps.core.views.company_views import CompanyViewSet + +urlpatterns = [ + path('company/create-onboarding/', CompanyViewSet.as_view({'post': 'create_onboarding'}), + name='company-create-onboarding'), +] diff --git a/apps/core/urls/dropdown_urls.py b/apps/core/urls/dropdown_urls.py new file mode 100644 index 0000000..b88ba67 --- /dev/null +++ b/apps/core/urls/dropdown_urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from apps.core.views.admin_views import CombinedBreakdownView + +urlpatterns = [ + path('admin/combined-breakdown/', CombinedBreakdownView.as_view(), name='combined-breakdown'), +] \ No newline at end of file diff --git a/apps/core/urls/email_confirmation_urls.py b/apps/core/urls/email_confirmation_urls.py new file mode 100644 index 0000000..12bd7ea --- /dev/null +++ b/apps/core/urls/email_confirmation_urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from apps.core.views.email_confirmation_views import ConfirmEmailAPIView + +urlpatterns = [ + path('confirm-email///', ConfirmEmailAPIView.as_view(), name='confirm-email'), +] \ No newline at end of file diff --git a/apps/core/urls/open_doors_urls.py b/apps/core/urls/open_doors_urls.py new file mode 100644 index 0000000..c4af341 --- /dev/null +++ b/apps/core/urls/open_doors_urls.py @@ -0,0 +1,14 @@ +from django.urls import path +from apps.core.views.open_doors_views import UserManagementView + +urlpatterns = [ + path('open-doors/register/', UserManagementView.as_view({'post': 'create'}), name='open-doors-register'), + path('open-doors/confirm-agreement/', UserManagementView.as_view({'post': 'service_agreement'}), + name='open-doors-confirm-agreement'), + path('open-doors/update-profile/', UserManagementView.as_view({'patch': 'partial_update'}), + name='open-doors-update-profile'), + path('open-doors/submit-report/', UserManagementView.as_view({'post': 'post_review_submission'}), + name='open-doors-submit-report'), + path('open-doors/get-report//', UserManagementView.as_view({'get': 'get_review_submission'}), + name='open-doors-get-report'), +] diff --git a/apps/core/urls/talent_urls.py b/apps/core/urls/talent_urls.py new file mode 100644 index 0000000..fbdfc1f --- /dev/null +++ b/apps/core/urls/talent_urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from apps.core.views.talent_views import TalentListView, TalentDetailView + +urlpatterns = [ + path('talents/', TalentListView.as_view(), name='talent-list'), + path('talents//', TalentDetailView.as_view(), name='talent-detail'), +] \ No newline at end of file diff --git a/apps/core/urls/user_urls.py b/apps/core/urls/user_urls.py new file mode 100644 index 0000000..b8e50a3 --- /dev/null +++ b/apps/core/urls/user_urls.py @@ -0,0 +1,23 @@ +from django.urls import path +from apps.core.views.user_views import UserDataView, ProfileUpdateView, get_announcement, UserProfileManagementView, \ + CompanyRegistrationView, UserRegistrationView, MemberCreationView + +urlpatterns = [ + path('user-data/', UserDataView.as_view(), name='user-data'), + path('update-profile/', ProfileUpdateView.as_view(), name='update-profile'), + path('register/', UserRegistrationView.as_view(), name='register-user'), + path('register-company/', CompanyRegistrationView.as_view(), name='register-company'), + path('update-notifications/', UserProfileManagementView.as_view({'post': 'update_notifications'}), + name='update-notifications'), + path('update-identity/', UserProfileManagementView.as_view({'post': 'update_identity'}), name='update-identity'), + path('update-social-accounts/', UserProfileManagementView.as_view({'post': 'update_social_accounts'}), + name='update-social-accounts'), + path('update-skills-roles/', UserProfileManagementView.as_view({'post': 'update_skills_roles'}), + name='update-skills-roles'), + path('update-work-place/', UserProfileManagementView.as_view({'post': 'update_work_place'}), + name='update-work-place'), + path('update-account-details/', UserProfileManagementView.as_view({'post': 'update_account_details'}), + name='update-account-details'), + path('announcement/', get_announcement, name='announcement'), + path('create-new-member/', MemberCreationView.as_view(), name='create-new-member') +] diff --git a/apps/core/urls_auth.py b/apps/core/urls_auth.py deleted file mode 100644 index bbd8cce..0000000 --- a/apps/core/urls_auth.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from .views_auth import UserPermissionAPIView, PasswordResetRequestView, PasswordResetConfirmView - -urlpatterns = [ - path("", UserPermissionAPIView.as_view(), name="auth"), - path('password-reset/', PasswordResetRequestView.as_view(), name='password_reset'), - path('password-reset-confirm///', PasswordResetConfirmView.as_view(), name='password_reset_confirm'), -] diff --git a/apps/core/urls_core.py b/apps/core/urls_core.py deleted file mode 100644 index 2373f43..0000000 --- a/apps/core/urls_core.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path - -from . import views_core -from .views_admin import CombinedBreakdownView - -urlpatterns = [ - path("details/", views_core.get_dropdown_data, name="details"), - path("member/all/", views_core.get_all_members, name="members"), - path('all-breakdowns/', CombinedBreakdownView.as_view(), name='all-breakdowns'), -] diff --git a/apps/core/urls_internal.py b/apps/core/urls_internal.py deleted file mode 100644 index e2c082f..0000000 --- a/apps/core/urls_internal.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path - -from apps.core.views_internal import ExternalView - -from . import views_internal - -urlpatterns = [ - path("user-demo/", ExternalView.as_view(), name="user-demo"), - path("update/review-token/", views_internal.ExternalView.update_review_token_total) -] diff --git a/apps/core/urls_member.py b/apps/core/urls_member.py deleted file mode 100644 index 2771b6f..0000000 --- a/apps/core/urls_member.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import path -from .views_member import MemberDetailsView - -urlpatterns = [ - path("member-details//", MemberDetailsView.as_view(), name="member-details") -] diff --git a/apps/core/urls_new.py b/apps/core/urls_new.py new file mode 100644 index 0000000..f65ef83 --- /dev/null +++ b/apps/core/urls_new.py @@ -0,0 +1,12 @@ +from django.urls import path, include + +urlpatterns = [ + path('', include('apps.core.urls.auth_urls')), + path('', include('apps.core.urls.user_urls')), + path('', include('apps.core.urls.talent_urls')), + path('', include('apps.core.urls.company_urls')), + path('', include('apps.core.urls.admin_urls')), + path('', include('apps.core.urls.dropdown_urls')), + path('', include('apps.core.urls.open_doors_urls')), + path('', include('apps.core.urls.email_confirmation_urls')), +] \ No newline at end of file diff --git a/apps/core/urls_open_doors.py b/apps/core/urls_open_doors.py deleted file mode 100644 index fb5425a..0000000 --- a/apps/core/urls_open_doors.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path, include -from rest_framework.routers import DefaultRouter - -from apps.core.views_open_doors import UserManagementView - -router = DefaultRouter() -router.register(r'onboarding', UserManagementView, basename='onboarding') - -urlpatterns = [ - path('', include(router.urls)), -] diff --git a/apps/core/urls_talent_choice.py b/apps/core/urls_talent_choice.py deleted file mode 100644 index 62ca437..0000000 --- a/apps/core/urls_talent_choice.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.urls import path, re_path -from .views_member import MemberDetailsView -from .views_talent_choice import CompanyViewSet, ConfirmEmailAPIView - -urlpatterns = [ - path("member-details//", MemberDetailsView.as_view(), name="member-details"), - path("confirm-agreement/", CompanyViewSet.as_view({ - 'post': 'service_agreement' - }), name="service-agreement"), - path("complete-onboarding/", CompanyViewSet.as_view({ - 'post': 'complete_onboarding' - }), name="complete-onboarding"), - path("onboarding/create/profile/", CompanyViewSet.as_view({ - 'post': 'create_onboarding' - }), name="create-onboarding"), - re_path(r'^confirm-email/(?P[^/.]+)/(?P[^/.]+)/$', - ConfirmEmailAPIView.as_view(), - name="confirm-account-email"), -] diff --git a/apps/core/views.py b/apps/core/views.py deleted file mode 100644 index 3e1cf2b..0000000 --- a/apps/core/views.py +++ /dev/null @@ -1,873 +0,0 @@ -import json -import logging -import os - -import requests -from django.contrib.auth import user_logged_out -from django.contrib.auth.hashers import make_password -from django.contrib.auth.tokens import default_token_generator -from django.contrib.sites.shortcuts import get_current_site -from django.core.mail import EmailMessage -from django.db import transaction -from django.http import JsonResponse -from django.template.loader import render_to_string -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode -from django.views.decorators.csrf import csrf_exempt -from knox.auth import AuthToken, TokenAuthentication -from rest_framework import status -from rest_framework.decorators import ( - api_view, - throttle_classes, - parser_classes, - permission_classes, -) -from rest_framework.generics import get_object_or_404 -from rest_framework.parsers import MultiPartParser -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.response import Response -from rest_framework.throttling import UserRateThrottle -from rest_framework.views import APIView - -from apps.company.models import Roles, CompanyProfile, Skill, Department -from apps.core.models import ( - UserProfile, - EthicIdentities, - GenderIdentities, - SexualIdentities, - CustomUser, -) -from apps.core.serializers import ( - UserProfileSerializer, - CustomAuthTokenSerializer, - UpdateProfileAccountDetailsSerializer, - CompanyProfileSerializer, - TalentProfileSerializer, -) -from apps.core.util import ( - extract_user_data, - extract_company_data, - extract_profile_data, - extract_talent_data, - update_user, - update_talent_profile, - update_user_profile, - create_or_update_company_connection, -) -from apps.mentorship.models import MentorshipProgramProfile, MentorRoster, MenteeProfile, MentorProfile -from apps.mentorship.serializer import ( - MentorRosterSerializer, - MentorshipProgramProfileSerializer, -) -from apps.member.models import MemberProfile -from utils.emails import send_dynamic_email -from utils.helper import prepend_https_if_not_empty -from utils.slack import fetch_new_posts, send_invite - -logger = logging.getLogger(__name__) - - -class LoginThrottle(UserRateThrottle): - rate = "5/min" - - -@api_view(["POST"]) -@throttle_classes([LoginThrottle]) -@permission_classes([AllowAny]) -def login_api(request): - serializer = CustomAuthTokenSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.validated_data["user"] - # userprofile = UserProfile.objects.get(user=user.id) - # userprofile_serializer = UserProfileSerializer(userprofile) - # userprofile_json_data = userprofile_serializer.data - # userprofile.timezone = request.data['timezone'] - # userprofile.save() - # create a token to track login - _, token = AuthToken.objects.create(user) - - response = JsonResponse( - { - "status": True, - "user_info": { - "id": user.id, - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "userprofile": [], - # 'userprofile': userprofile_json_data - }, - "account_info": { - "is_staff": user.is_staff, - "is_recruiter": user.is_recruiter, - "is_member": user.is_member, - "is_mentor": user.is_mentor, - "is_mentee": user.is_mentee, - "is_speaker": user.is_speaker, - "is_volunteer": user.is_volunteer, - "is_mentor_profile_active": user.is_mentor_profile_active, - "is_mentor_training_complete": user.is_mentor_training_complete, - "is_mentor_profile_approved": user.is_mentor_profile_approved, - "is_mentor_application_submitted": user.is_mentor_application_submitted, - "is_talent_source_beta": user.is_talent_source_beta, - "is_team": user.is_team, - "is_community_recruiter": user.is_community_recruiter, - "is_company_account": user.is_company_account, - "is_partnership": user.is_partnership, - "is_company_review_access_active": user.is_company_review_access_active, - }, - "token": token, - } - ) - - # Set secure cookie - response.set_cookie( - "auth_token", token, secure=False, httponly=True, domain=os.environ["FRONTEND_URL"] - ) # httponly=True to prevent access by JavaScript - - return response - - -@api_view(["GET"]) -def get_user_data(request): - user = request.user - userprofile = get_object_or_404(UserProfile, user=user) - userprofile_json_data = UserProfileSerializer(userprofile).data - - # Initialize empty data structures for optional response data - mentor_data, mentee_data, mentor_roster_data = {}, {}, {} - talentprofile_json_data = None - - response_data = { - "status": True, - "user_info": { - "id": user.id, - "first_name": user.first_name, - "last_name": user.last_name, - "email": user.email, - "userprofile": userprofile_json_data, - # "talentprofile" and "current_company" will be conditionally added - }, - "account_info": {field: getattr(user, field) for field in [ - "is_staff", "is_recruiter", "is_member", "is_member_onboarding_complete", - "is_mentor", "is_mentee", "is_mentor_profile_active", "is_open_doors", - "is_open_doors_onboarding_complete", "is_mentor_profile_removed", "is_mentor_training_complete", - "is_mentor_interviewing", "is_mentor_profile_paused", - "is_community_recruiter", "is_company_account", "is_email_confirmation_sent", - "is_email_confirmed", "is_company_onboarding_complete", - "is_mentor_profile_approved", "is_mentor_application_submitted", - "is_speaker", "is_volunteer", "is_team", "is_community_recruiter", - "is_company_account", "is_partnership", "is_company_review_access_active" - ]}, - "mentor_details": mentor_data, - "mentee_details": mentee_data, - "mentor_roster_data": mentor_roster_data, - } - - # Conditional data based on user's roles - if user.is_mentor_application_submitted: - mentor_application = MentorshipProgramProfile.objects.get(user=user) - response_data["mentor_data"] = MentorshipProgramProfileSerializer(mentor_application).data - - if user.is_mentee: - mentee_profile = get_object_or_404(MenteeProfile, user=user) - mentee_data = {"id": mentee_profile.id} - mentorship_roster = MentorRoster.objects.filter(mentee=mentee_profile) - if mentorship_roster.exists(): - response_data["mentor_roster_data"] = MentorRosterSerializer(mentorship_roster, many=True).data - - talent_profile = MemberProfile.objects.filter(user=user).first() - if talent_profile: - talentprofile_json_data = TalentProfileSerializer(talent_profile).data - response_data["user_info"]["talentprofile"] = talentprofile_json_data - - # Handle company account logic - if user.is_company_account: - company_account_details = get_object_or_404(CompanyProfile, account_owner=user) - local_company_data = CompanyProfileSerializer(company_account_details).data - - # Make an external request for additional company details - company_id = company_account_details.id - full_company_details_url = f'{os.environ["TC_API_URL"]}core/api/company/details/?company_id={company_id}' - response = requests.get(full_company_details_url) - if response.status_code == 200: - company_account_data = response.json() - # Append local company data to the fetched company data - company_account_data["company_profile"] = local_company_data - else: - company_account_data = {"error": "Could not fetch company details"} - - response_data["company_account_data"] = company_account_data - - return Response(response_data) - - -def get_company_data(user_details): - company = get_object_or_404(CompanyProfile, account_owner=user_details) - return CompanyProfileSerializer(company).data - - -@api_view(["GET"]) -def get_announcement(request): - try: - slack_msg = fetch_new_posts("CELK4L5FW", 1) - if slack_msg: - return Response({"announcement": slack_msg}, status=status.HTTP_200_OK) - else: - print(f"Did not get a new slack message") - return Response( - {"message": "No new messages."}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - print(f"Error pulling slack message: {str(e)}") - return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - -# @login_required -@parser_classes([MultiPartParser]) -@api_view(["POST"]) -def create_new_member(request): - if request.user.is_member_onboarding_complete: - return Response( - { - "status": False, - "message": "Member has already been created for this user.", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - try: - data = request.data - user_data = extract_user_data(data) - company_data = extract_company_data(data) - profile_data = extract_profile_data(data, request.FILES) - talent_data = extract_talent_data(data, request.FILES) - - with transaction.atomic(): - user = update_user(request.user, user_data) - talent_profile = update_talent_profile(user, talent_data) - user_profile = update_user_profile(user, profile_data) - user_company_connection = create_or_update_company_connection( - user, company_data - ) - - if user_data["is_mentee"] or user_data["is_mentor"]: - mentorship_program = MentorshipProgramProfile.objects.create(user=user) - request.user.is_mentee = user_data["is_mentee"] - request.user.is_mentor = user_data["is_mentor"] - if user_data["is_mentor"]: - mentor_profile = MentorProfile.objects.create(user=request.user) - mentorship_program.mentor_profile = mentor_profile - mentorship_program.save() - - template_id = "d-96a6752bd6b74888aa1450ea30f33a06" - dynamic_template_data = {"first_name": request.user.first_name} - - email_data = { - "subject": "Welcome to Our Platform", - "recipient_emails": [request.user.email], - "template_id": template_id, - "dynamic_template_data": dynamic_template_data, - } - send_dynamic_email(email_data) - request.user.is_member_onboarding_complete = True - request.user.is_company_review_access_active = True - request.user.save() - # send slack invite - try: - send_invite(user.email) - request.user.is_slack_invite_sent = True - request.user.save() - except Exception as e: - request.user.is_slack_invite_sent = False - print(e) - - return Response( - { - "status": True, - "message": "User, MemberProfile, and UserProfile created successfully!", - }, - status=status.HTTP_200_OK, - ) - - except Exception as e: - # Handle specific known exceptions - return Response( - {"status": False, "error": str(e)}, status=status.HTTP_400_BAD_REQUEST - ) - except Exception as e: - # Handle unexpected exceptions - print(e) - return Response( - {"status": False, "error": "An unexpected error occurred."}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - -@api_view(["GET"]) -def get_new_company_data(request): - return Response( - { - "status": True, - "data": [ - { - "step": "Marketing Related Questions", - "questions": [ - { - "order": 0, - "label": "Communication Settings", - "key": None, - "helper_text": "The following questions will help us understand what email and updates you want form us.", - "type": "title", - "options": None, - }, - { - "order": 1, - "label": "Please details your would like to receive marketing about", - "key": None, - "helper_text": None, - "type": "header", - "options": None, - }, - { - "order": 2, - "label": "Our Monthly Newsletter", - "key": "marketing_monthly_newsletter", - "helper_text": None, - "type": "checkbox", - "options": None, - }, - { - "order": 3, - "label": "Community Events", - "key": "marketing_events", - "helper_text": None, - "type": "checkbox", - "options": None, - }, - { - "order": 4, - "label": "Interest Based Programing", - "key": "marketing_identity_based_programing", - "helper_text": None, - "type": "checkbox", - "options": None, - }, - { - "order": 5, - "label": "Open Jobs & Job Hunting Tips", - "key": "marketing_jobs", - "helper_text": None, - "type": "checkbox", - "options": None, - }, - { - "order": 5, - "label": "Community Updates", - "key": "marketing_org_updates", - "helper_text": None, - "type": "checkbox", - "options": None, - }, - ], - } - ], - } - ) - - -@api_view(["POST"]) -def update_profile_account_details(request): - """ - Update the account details associated with a user's profile. - - This view function handles a POST request to update various fields of a user's profile. It leverages - Django Rest Framework's serializer for data validation and saving. If the profile associated with the - user does not exist, it returns an appropriate response. - - Parameters: - - request: The HttpRequest object containing the POST data and the logged-in user's information. - - Returns: - - Response: A DRF Response object. If the update is successful, it returns a success status and message. - If the profile does not exist, it returns a 404 Not Found status with an error message. - If the provided data is invalid, it returns a 400 Bad Request status with error details. - - Raises: - - MemberProfile.DoesNotExist: If the UserProfile associated with the user does not exist. - """ - user = request.user - try: - profile = user.userprofile - except MemberProfile.DoesNotExist: - return Response( - {"status": False, "message": "Profile not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if request.method == "POST": - serializer = UpdateProfileAccountDetailsSerializer( - profile, data=request.data, partial=True - ) - if serializer.is_valid(): - serializer.save() - return Response( - {"status": True, "message": "Form Saved"}, status=status.HTTP_200_OK - ) - return Response( - {"status": False, "message": serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - -@api_view(["POST"]) -def update_profile_work_place(request): - # Handling existing company. - company_details = request.data.get("select_company", None) - - if company_details: - try: - company = CompanyProfile.objects.get(id=company_details["id"]) - except CompanyProfile.DoesNotExist: - return Response( - {"status": False, "detail": "Company does not exist."}, - status=status.HTTP_400_BAD_REQUEST, - ) - else: # Handling new company. - company_serializer = CompanyProfileSerializer( - data=request.data, context={"request": request} - ) - if company_serializer.is_valid(): - company = company_serializer.save() - else: - return Response( - {"status": False, "message": company_serializer.errors}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Updating the current employee for the company. - user = request.user - company.current_employees.add(user) - company.save() - - # Updating talent profile. - talent_profile = get_object_or_404(MemberProfile, user=request.user) - role_names = request.data.get("job_roles") - - roles_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for role_name in role_names: - try: - # Try to get the role by name, and if it doesn't exist, create it. - role, created = Roles.objects.get_or_create(name=role_name["name"]) - roles_to_set.append(role) - except (Roles.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid role: {role_name}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - talent_profile.role.set(roles_to_set) - talent_profile.save() - - return Response({"detail": "Account Details Updated."}, status=status.HTTP_200_OK) - - -@api_view(["POST"]) -def update_profile_skills_roles(request): - userprofile = request.user - roles = request.data.get("department") - skills = request.data.get("skills") - - roles_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for role_name in roles: - try: - # Try to get the role by name, and if it doesn't exist, create it. - role = Department.objects.get(name=role_name["name"]) - roles_to_set.append(role) - except (Department.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid department: {role_name}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - skills_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for skill in skills: - try: - # Try to get the role by name, and if it doesn't exist, create it. - if isinstance(skill, str): - name = Skill.objects.get(name=skill) - else: - name = Skill.objects.get(name=skill["name"]) - skills_to_set.append(name.pk) - except (Skill.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid skills: {skill}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if roles_to_set: - userprofile.user.department.set(roles_to_set) - if skills_to_set: - userprofile.user.skills.set(skills_to_set) - userprofile.save() - - return Response( - {"status": True, "detail": "Account Details Updated."}, - status=status.HTTP_200_OK, - ) - - -@api_view(["POST"]) -def update_profile_social_accounts(request): - userprofile = request.user.userprofile - userprofile.linkedin = prepend_https_if_not_empty(request.data.get("linkedin")) - userprofile.instagram = request.data.get("instagram", None) - userprofile.github = prepend_https_if_not_empty(request.data.get("github")) - userprofile.twitter = request.data.get("twitter", None) - userprofile.youtube = prepend_https_if_not_empty(request.data.get("youtube")) - userprofile.personal = prepend_https_if_not_empty(request.data.get("personal")) - userprofile.save() - - return Response( - {"status": True, "detail": "Account Details Updated."}, - status=status.HTTP_200_OK, - ) - - -@api_view(["POST"]) -def update_profile_identity(request): - # TODO | [CODE CLEAN UP] MOVE TO SERIALIZER - userprofile = request.user - - identity_sexuality = request.data.get("identity_sexuality") - gender_identities = request.data.get("gender_identities") - ethic_identities = request.data.get("ethic_identities") - disability = request.data.get("disability") - care_giver = request.data.get("care_giver") - veteran_status_str = request.data.get("veteran_status") - - sexuality_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for role_name in identity_sexuality: - try: - # Try to get the role by name, and if it doesn't exist, create it. - role = SexualIdentities.objects.get(name=role_name) - sexuality_to_set.append(role) - except (SexualIdentities.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid sexuality: {role_name}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - gender_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for role_name in gender_identities: - try: - # Try to get the role by name, and if it doesn't exist, create it. - role = GenderIdentities.objects.get(name=role_name) - gender_to_set.append(role) - except (Roles.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid name: {role_name}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - ethic_to_set = ( - [] - ) # This list will hold the role objects to be set to the MemberProfile - for role_name in ethic_identities: - try: - # Try to get the role by name, and if it doesn't exist, create it. - role = EthicIdentities.objects.get(name=role_name) - ethic_to_set.append(role) - except (Roles.MultipleObjectsReturned, ValueError): - # Handle the case where multiple roles are found with the same name or - # where the name is invalid (for instance, if name is a required field - # and it's None or an empty string). - return Response( - {"detail": f"Invalid name: {role_name}"}, - status=status.HTTP_400_BAD_REQUEST, - ) - if sexuality_to_set: - userprofile.userprofile.identity_sexuality.set(sexuality_to_set) - if gender_to_set: - userprofile.userprofile.identity_gender.set(gender_to_set) - if ethic_to_set: - userprofile.userprofile.identity_ethic.set(ethic_to_set) - if disability: - userprofile.userprofile.disability = bool(disability) - if care_giver: - userprofile.userprofile.care_giver = bool(care_giver) - if veteran_status_str: - userprofile.userprofile.veteran_status = veteran_status_str - - userprofile.userprofile.is_identity_sexuality_displayed = request.data.get( - "is_identity_sexuality_displayed" - ) - userprofile.userprofile.is_identity_gender_displayed = request.data.get( - "is_identity_gender_displayed" - ) - userprofile.userprofile.is_identity_ethic_displayed = request.data.get( - "is_identity_ethic_displayed" - ) - userprofile.userprofile.is_disability_displayed = request.data.get( - "is_disability_displayed" - ) - userprofile.userprofile.is_care_giver_displayed = request.data.get( - "is_care_giver_displayed" - ) - userprofile.userprofile.is_veteran_status_displayed = request.data.get( - "is_veteran_status_displayed" - ) - - userprofile.save() - userprofile.userprofile.save() - - return Response( - {"status": True, "detail": "Account Details Updated."}, - status=status.HTTP_200_OK, - ) - - -@api_view(["POST"]) -def update_profile_notifications(request): - userprofile = request.user.userprofile - - marketing_jobs = request.data.get("marketing_jobs") - marketing_events = request.data.get("marketing_events") - marketing_org_updates = request.data.get("marketing_org_updates") - marketing_identity_based_programing = request.data.get( - "marketing_identity_based_programing" - ) - marketing_monthly_newsletter = request.data.get("marketing_monthly_newsletter") - - userprofile.marketing_jobs = bool(marketing_jobs) - userprofile.marketing_events = bool(marketing_events) - userprofile.marketing_org_updates = bool(marketing_org_updates) - userprofile.marketing_identity_based_programing = bool( - marketing_identity_based_programing - ) - userprofile.marketing_monthly_newsletter = bool(marketing_monthly_newsletter) - - userprofile.save() - - return Response( - {"status": True, "detail": "Account Details Updated."}, - status=status.HTTP_200_OK, - ) - - -@csrf_exempt -def create_new_user(request): - """ - Create a new user. This view handles the POST request to register a new user. - It performs input validation, user creation, and sending a welcome email. - """ - if request.method != "POST": - return JsonResponse( - {"status": False, "error": "Invalid request method"}, status=405 - ) - - data = json.loads(request.body) - first_name, last_name, email, password = ( - data.get("first_name"), - data.get("last_name"), - data.get("email", "").lower(), - data.get("password"), - ) - - if not all([first_name, last_name, email, password]): - return JsonResponse( - {"status": False, "error": "Missing required parameters"}, status=400 - ) - - if CustomUser.objects.filter(email=email).exists(): - return JsonResponse( - {"status": False, "message": "Email already in use"}, status=400 - ) - try: - user, token = create_user_account(first_name, last_name, email, password) - try: - send_welcome_email(user.email, user.first_name) - user.is_email_confirmation_sent = True - user.save() - return JsonResponse({"status": True, "message": "User created successfully", "token": token}, status=201) - except Exception as e: - print("Error while saving user: ", str(e)) - return JsonResponse({"status": False, "message": "Unable to create user"}, status=500) - except Exception as e: - print("Error while saving user: ", str(e)) - return JsonResponse({"status": False, "message": "Unable to create user"}, status=500) - - -@csrf_exempt -def create_new_company(request): - """ - Create a new company user. This view handles the POST request to register a new user. - It performs input validation, user creation, and sending a welcome email. - """ - if request.method != "POST": - return JsonResponse( - {"status": False, "error": "Invalid request method"}, status=405 - ) - - try: - data = json.loads(request.body) - except json.JSONDecodeError as e: - logger.error("Error decoding JSON data: %s", str(e)) - return JsonResponse({"status": False, "error": "Invalid JSON data"}, status=400) - - first_name, last_name, email, password, company_name = ( - data.get("first_name"), - data.get("last_name"), - data.get("email", "").lower(), - data.get("password"), - data.get("company_name"), - ) - - if not all([first_name, last_name, email, password, company_name]): - return JsonResponse( - {"status": False, "error": "Missing required parameters"}, status=400 - ) - - if CustomUser.objects.filter(email=email).exists(): - return JsonResponse( - {"status": False, "message": "Email already in use"}, status=400 - ) - - try: - with transaction.atomic(): - user, token = create_user_account(first_name, last_name, email, password, is_company=True) - company_profile = CompanyProfile( - account_creator=user, - company_name=company_name - ) - company_profile.save() - company_profile.account_owner.add(user) - company_profile.hiring_team.add(user) - header_token = request.headers.get("Authorization", None) - - try: - response = requests.post( - f'{os.environ["TC_API_URL"]}company/new/onboarding/create-accounts/', - data=json.dumps({"companyId": company_profile.id}), - headers={'Content-Type': 'application/json'}, verify=True) - response.raise_for_status() - except requests.RequestException as e: - logger.error("Failed to create external accounts: %s", str(e)) - transaction.set_rollback(True) - return JsonResponse( - {"status": False, "error": "Failed to communicate with external service"}, - status=502 # Bad Gateway indicates issues with external service - ) - - try: - send_welcome_email(user.email, user.first_name, company_name, user, get_current_site(request), request) - except Exception as e: - logger.error("Failed to send welcome email: %s", str(e)) - transaction.set_rollback(True) - return JsonResponse( - {"status": False, "error": "Failed to send welcome email"}, - status=500 - ) - - return JsonResponse({"status": True, "message": "User created successfully", "token": token}, status=201) - - except Exception as e: - logger.error("Error while creating user: %s", str(e)) - return JsonResponse({"status": False, "message": "Unable to create user"}, status=500) - - -class LogoutView(APIView): - authentication_classes = (TokenAuthentication,) - permission_classes = (IsAuthenticated,) - - def post(self, request, format=None): - request._auth.delete() - user_logged_out.send( - sender=request.user.__class__, request=request, user=request.user - ) - return Response(None, status=status.HTTP_204_NO_CONTENT) - - -def create_user_account(first_name, last_name, email, password, is_company=False, request=None): - """ - Create a new CustomUser account. - """ - user = CustomUser( - first_name=first_name, - last_name=last_name, - email=email, - password=make_password(password), - is_company_account=is_company - ) - user.save() - _, token = AuthToken.objects.create(user) - if not is_company: - user.is_member = True - - user.save() - return user, token - - -def send_welcome_email(email, first_name, company_name=None, user=None, current_site=None, request=None): - """ - Send a welcome email to the new user. - """ - if company_name: - token = default_token_generator.make_token(user) - - # Create the email - mail_subject = 'Activate your account.' - activation_link = f'{os.environ["FRONTEND_URL"]}new/confirm-account/{urlsafe_base64_encode(force_bytes(user.pk))}/{token}/' - - context = { - 'username': first_name, - 'activation_link': activation_link, - } - - message = render_to_string('emails/acc_active_email.txt', context=context) - email_msg = EmailMessage(mail_subject, message, 'notifications@app.techbychocie.org', [user.email]) - email_msg.extra_headers = { - 'email_template': 'emails/acc_active_email.html', - 'token': token, - 'username': first_name, - 'activation_link': activation_link, - } - try: - email_msg.send() - except Exception as e: - print("Error while sending emails: ", str(e)) - else: - template_id = "d-342822c240ed43778ba9e94a04fb10cf" - dynamic_template_data = {"first_name": first_name} - - email_data = { - "subject": "Welcome to Our Platform", - "recipient_emails": [email], - "template_id": template_id, - "dynamic_template_data": dynamic_template_data, - } - - send_dynamic_email(email_data) diff --git a/apps/core/views/admin_views.py b/apps/core/views/admin_views.py new file mode 100644 index 0000000..ebc1280 --- /dev/null +++ b/apps/core/views/admin_views.py @@ -0,0 +1,56 @@ +from django.db.models import Count +from rest_framework.permissions import IsAdminUser +from rest_framework.views import APIView + +from apps.core.models import CustomUser, SexualIdentities, GenderIdentities, EthicIdentities, PronounsIdentities +from apps.company.models import Department, Roles, Industries, Skill, CompanyProfile +from apps.member.models import MemberProfile +from apps.mentorship.models import MentorProfile +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +class CombinedBreakdownView(APIView): + permission_classes = [IsAdminUser] + + @log_exception(logger) + @timed_function(logger) + def get(self, request): + tech_journey_counts = MemberProfile.objects.values('tech_journey').annotate( + count=Count('tech_journey')).order_by('tech_journey') + + for item in tech_journey_counts: + item['name'] = dict(MemberProfile.CAREER_JOURNEY).get(item['tech_journey'], 'Unknown') + + response_data = { + 'skills': self.get_annotated_data(Skill, 'talent_skills_list'), + 'departments': self.get_annotated_data(Department, 'talent_department_list'), + 'roles': self.get_annotated_data(Roles, 'talent_role_types'), + 'industries': self.get_annotated_data(Industries, 'member_industries'), + 'identity_sexuality': self.get_annotated_data(SexualIdentities, 'userprofile_identity_sexuality'), + 'identity_gender': self.get_annotated_data(GenderIdentities, 'userprofile_identity_gender'), + 'identity_ethic': self.get_annotated_data(EthicIdentities, 'userprofile_identity_ethic'), + 'identity_pronouns': self.get_annotated_data(PronounsIdentities, 'userprofile_identity_pronouns'), + 'total_member': CustomUser.objects.filter(is_member=True).count(), + 'total_member_level': tech_journey_counts, + 'total_member_talent_choice': CustomUser.objects.filter(is_talent_choice=True).count(), + 'talent_choice_job_roles_needed': self.get_annotated_data( + Roles, 'talent_role_types', + extra_filter={'talent_role_types__user__is_talent_choice': True} + ), + 'total_company_talent_choice': CompanyProfile.objects.filter(talent_choice_account=True).count(), + 'total_active_mentors': MentorProfile.objects.filter(mentor_status="active").count(), + 'total_mentors_applications': MentorProfile.objects.filter(mentor_status="submitted").count(), + 'total_mentors_interviewing': MentorProfile.objects.filter(mentor_status="interviewing").count(), + 'total_mentors_need_cal_info': MentorProfile.objects.filter(mentor_status="need_cal_info").count(), + } + + return api_response(data=response_data, message="Combined breakdown data retrieved successfully") + + def get_annotated_data(self, model, related_name, extra_filter=None): + queryset = model.objects.annotate(members_count=Count(related_name)) + if extra_filter: + queryset = queryset.filter(**extra_filter) + return list(queryset.filter(members_count__gt=0).order_by('-members_count').values('name', 'members_count')) diff --git a/apps/core/views/auth_views.py b/apps/core/views/auth_views.py new file mode 100644 index 0000000..292010c --- /dev/null +++ b/apps/core/views/auth_views.py @@ -0,0 +1,119 @@ +import os + +from django.contrib.auth import user_logged_out +from django.contrib.auth.tokens import default_token_generator +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from knox.auth import TokenAuthentication +from knox.models import AuthToken +from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.views import APIView + +from apps.core.models import CustomUser +from apps.core.serialiizers.password_reset import SetNewPasswordSerializer, PasswordResetSerializer +from apps.core.serializers.user_serializers import UserAccountInfoSerializer, CustomAuthTokenSerializer, \ + BaseUserSerializer +from utils.api_helpers import api_response +from utils.emails import send_password_email +from utils.logging_helper import get_logger, log_exception, timed_function + +logger = get_logger(__name__) + + +class UserPermissionAPIView(APIView): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def get(self, request): + permissions = request.user.is_authenticated + return api_response(data={'permissions': permissions}, message="User permissions retrieved") + + +class LoginView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + serializer = CustomAuthTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.validated_data['user'] + + _, token = AuthToken.objects.create(user) + + user_serializer = BaseUserSerializer(user) + account_info_serializer = UserAccountInfoSerializer(user) + + return api_response( + data={ + "token": token, + "user_info": user_serializer.data, + "account_info": account_info_serializer.data, + }, + message="Login successful" + ) + + +class LogoutView(APIView): + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + """ + Log out the user and delete the authentication token. + """ + request._auth.delete() + user_logged_out.send( + sender=request.user.__class__, request=request, user=request.user + ) + return api_response(message="Logout successful", status_code=status.HTTP_204_NO_CONTENT) + + +class PasswordResetRequestView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + """ + Send a password reset request to the user via email + """ + serializer = PasswordResetSerializer(data=request.data) + if serializer.is_valid(): + user = CustomUser.objects.get(email=serializer.validated_data['email']) + uidb64 = urlsafe_base64_encode(force_bytes(user.pk)) + token = default_token_generator.make_token(user) + reset_link = f"{os.getenv('FRONTEND_URL')}password-reset/confirm-password/{uidb64}/{token}" + + email_data = { + "recipient_emails": [user.email], + "template_id": "your_password_reset_template_id", + "dynamic_template_data": { + "username": user.first_name, + "reset_link": reset_link, + }, + } + send_password_email(user.email, user.first_name, user, reset_link) + + return api_response(message="Password reset link sent.") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + +class PasswordResetConfirmView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def post(self, request, uidb64, token): + """ + Update user password if token is valid + """ + serializer = SetNewPasswordSerializer(data=request.data, context={'uidb64': uidb64, 'token': token}) + if serializer.is_valid(): + return api_response(message="Password has been reset.") + return api_response(errors=serializer.errors, message="Error: We could not update your password.", + status_code=status.HTTP_400_BAD_REQUEST) diff --git a/apps/core/views/company_views.py b/apps/core/views/company_views.py new file mode 100644 index 0000000..886b51e --- /dev/null +++ b/apps/core/views/company_views.py @@ -0,0 +1,47 @@ +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import ViewSet + +from apps.company.models import CompanyProfile +from apps.core.serializers.company_serializers import CompanyProfileSerializer +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.api_helpers import api_response +from utils.urls_utils import prepend_https_if_not_empty + +logger = get_logger(__name__) + + +class CompanyViewSet(ViewSet): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['post'], url_path='create-onboarding') + def create_onboarding(self, request): + user = request.user + company_data = { + 'company_name': request.data.get('company_name'), + 'company_url': prepend_https_if_not_empty(request.data.get('website', '')), + 'linkedin': prepend_https_if_not_empty(request.data.get('linkedin', '')), + 'twitter': prepend_https_if_not_empty(request.data.get('twitter', '')), + 'youtube': prepend_https_if_not_empty(request.data.get('youtube', '')), + 'facebook': prepend_https_if_not_empty(request.data.get('facebook', '')), + 'instagram': prepend_https_if_not_empty(request.data.get('instagram', '')), + 'location': request.data.get('location', ''), + 'city': request.data.get('city', ''), + 'state': request.data.get('state', ''), + 'postal_code': request.data.get('postalCode', ''), + 'mission': request.data.get('mission'), + 'company_size': request.data.get('company_size'), + } + + serializer = CompanyProfileSerializer(data=company_data, context={'request': request}) + if serializer.is_valid(): + company = serializer.save() + if 'logo' in request.FILES: + company.logo = request.FILES['logo'] + company.save() + return api_response(data={"companyId": company.id}, message="Company profile created successfully", + status_code=201) + else: + return api_response(errors=serializer.errors, message="Failed to create company profile", status_code=400) diff --git a/apps/core/views/dropdown_views.py b/apps/core/views/dropdown_views.py new file mode 100644 index 0000000..7aae451 --- /dev/null +++ b/apps/core/views/dropdown_views.py @@ -0,0 +1,63 @@ +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import AllowAny + +from apps.company.models import ( + Roles, CompanyProfile, CAREER_JOURNEY, Skill, Department, Industries, + CompanyTypes, SalaryRange, COMPANY_SIZE, ON_SITE_REMOTE, +) +from apps.core.models import ( + PronounsIdentities, GenderIdentities, SexualIdentities, EthicIdentities, + CommunityNeeds, UserProfile, +) +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.cache_utils import cache_decorator +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +@api_view(["GET"]) +@permission_classes([AllowAny]) +@log_exception(logger) +@timed_function(logger) +@cache_decorator(timeout=3600) # Cache for 1 hour +def get_dropdown_data(request): + data = {} + requested_fields = request.query_params.getlist("fields", []) + + field_mappings = { + "pronouns": (PronounsIdentities, "name", "id"), + "job_roles": (Roles, "name", "id"), + "companies": (CompanyProfile, "company_name", "id", "logo", "company_url"), + "job_skills": (Skill, "name", "id"), + "job_departments": (Department, "name", "id"), + "job_industries": (Industries, "name", "id"), + "company_types": (CompanyTypes, "name", "id"), + "gender": (GenderIdentities, "name", "id"), + "sexuality": (SexualIdentities, "name", "id"), + "ethic": (EthicIdentities, "name", "id"), + "job_salary_range": (SalaryRange, "range", "id"), + "community_needs": (CommunityNeeds, "name", "id"), + } + + for field, model_info in field_mappings.items(): + if not requested_fields or field in requested_fields: + model, *fields = model_info + data[field] = list(model.objects.order_by(fields[0]).values(*fields)) + + if not requested_fields or "years_of_experience" in requested_fields: + data["years_of_experience"] = [ + {"value": code, "label": description} + for code, description in CAREER_JOURNEY + ] + + if not requested_fields or "how_connected" in requested_fields: + data["how_connected"] = UserProfile.HOW_CONNECTION_MADE + + if not requested_fields or "company_size" in requested_fields: + data["company_size"] = COMPANY_SIZE + + if not requested_fields or "on_site_remote" in requested_fields: + data["on_site_remote"] = ON_SITE_REMOTE + + return api_response(data=data, message="Dropdown data retrieved successfully") diff --git a/apps/core/views/email_confirmation_views.py b/apps/core/views/email_confirmation_views.py new file mode 100644 index 0000000..e2a3585 --- /dev/null +++ b/apps/core/views/email_confirmation_views.py @@ -0,0 +1,48 @@ +from django.contrib.auth.tokens import default_token_generator +from django.utils.encoding import force_str +from django.utils.http import urlsafe_base64_decode +from knox.models import AuthToken +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.permissions import AllowAny + +from apps.core.models import CustomUser +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +class ConfirmEmailAPIView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def get(self, request, id=None, token=None): + try: + uid = force_str(urlsafe_base64_decode(id)) + user = CustomUser.objects.get(id=uid) + + if user.is_email_confirmed: + return api_response(message="Email already confirmed!") + + is_token_valid = default_token_generator.check_token(user, token) + + if is_token_valid: + user.is_active = True + user.is_email_confirmed = True + user.save() + _, token = AuthToken.objects.create(user) + return api_response(data={"token": token}, message="Email confirmed! Please complete your account.") + else: + return api_response(message="Invalid email token. Please contact support.", + status_code=status.HTTP_400_BAD_REQUEST) + + except CustomUser.DoesNotExist: + logger.warning(f"User does not exist for id: {id}") + return api_response(message="Invalid email token or user does not exist.", + status_code=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(f"Unhandled error: {e}") + return api_response(message="An unexpected error occurred.", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/apps/core/views/open_doors_views.py b/apps/core/views/open_doors_views.py new file mode 100644 index 0000000..ba5deb7 --- /dev/null +++ b/apps/core/views/open_doors_views.py @@ -0,0 +1,128 @@ +import os +import requests +from django.contrib.auth.tokens import default_token_generator +from django.utils.encoding import force_str +from django.utils.http import urlsafe_base64_decode +from knox.models import AuthToken +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet + +from apps.core.models import CustomUser, UserVerificationToken +from apps.core.serializers.user_serializers import UserRegistrationSerializer +from apps.core.serializers.profile_serializers import UserProfileSerializer +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.emails import send_dynamic_email +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +class UserManagementView(ViewSet): + @log_exception(logger) + @timed_function(logger) + def get_permissions(self): + if self.action == 'create': + permission_classes = [AllowAny] + else: + permission_classes = [IsAuthenticated] + return [permission() for permission in permission_classes] + + @log_exception(logger) + @timed_function(logger) + def create(self, request): + serializer = UserRegistrationSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + user.is_open_doors = True + user.is_company_review_access_active = True + user.save() + _, token = AuthToken.objects.create(user) + verification_token = UserVerificationToken.create_token(user) + frontend_url = os.getenv("FRONTEND_URL", "default_fallback_url") + verification_url = f"{frontend_url}?token={verification_token.token}" + + email_data = { + "recipient_emails": [user.email], + "template_id": "your_welcome_email_template_id", + "dynamic_template_data": { + "first_name": user.first_name, + "verification_url": verification_url, + }, + } + send_dynamic_email(email_data) + + return api_response( + data={"token": token}, + message="User registered. Please check your email.", + status_code=status.HTTP_201_CREATED + ) + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['post'], url_path='confirm-agreement') + @log_exception(logger) + @timed_function(logger) + def service_agreement(self, request): + if request.data.get('confirm_service_agreement'): + user = request.user + user.confirm_service_agreement = True + user.is_open_doors_onboarding_complete = True + user.is_company_review_access_active = True + user.save() + return api_response(message="Welcome to Open Doors.") + else: + return api_response( + message="Please accept the service agreement to create your account.", + status_code=status.HTTP_400_BAD_REQUEST + ) + + @log_exception(logger) + @timed_function(logger) + def partial_update(self, request): + user = request.user + serializer = UserProfileSerializer(user.userprofile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="User profile updated successfully.") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['post'], url_path='submit-report') + @log_exception(logger) + @timed_function(logger) + def post_review_submission(self, request): + user_id = request.user.id + header_token = request.headers.get("Authorization", None) + mutable_data = request.data.copy() + mutable_data['user_id'] = user_id + mutable_data['header_token'] = header_token + third_party_url = f'{os.getenv("OD_API_URL")}reports/submit-report/' + + try: + headers = { + "Content-Type": "application/json", + "Authorization": header_token + } + response = requests.post(third_party_url, json=mutable_data, headers=headers) + response.raise_for_status() + return api_response(data=response.json(), message="Report submitted successfully") + except requests.RequestException as e: + logger.error(f"Error submitting report: {str(e)}") + return api_response(message="Failed to submit report", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=['get'], url_path='get-report') + @log_exception(logger) + @timed_function(logger) + def get_review_submission(self, request, pk=None): + user_id = request.user.id + data = {"id": user_id} + third_party_url = f'{os.getenv("OD_API_URL")}reports/get/report/{pk}/' + + try: + response = requests.get(third_party_url, data=data) + response.raise_for_status() + return api_response(data=response.json(), message="Report retrieved successfully") + except requests.RequestException as e: + logger.error(f"Error retrieving report: {str(e)}") + return api_response(message="Failed to retrieve report", status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/apps/core/views/talent_views.py b/apps/core/views/talent_views.py new file mode 100644 index 0000000..bad49b1 --- /dev/null +++ b/apps/core/views/talent_views.py @@ -0,0 +1,38 @@ +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView + +from apps.core.serializers.talent_serializers import FullTalentProfileSerializer +from apps.member.models import MemberProfile +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.helper import CustomPagination, paginate_items +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +class TalentListView(APIView): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def get(self, request): + paginator = CustomPagination() + members = MemberProfile.objects.filter(user__is_active=True).order_by("created_at") + paginated_members = paginate_items(members, request, paginator, FullTalentProfileSerializer) + + return api_response(data={"members": paginated_members}, message="All members retrieved successfully") + + +class TalentDetailView(APIView): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def get(self, request, pk): + try: + member = MemberProfile.objects.get(pk=pk, user__is_active=True) + serializer = FullTalentProfileSerializer(member) + return api_response(data=serializer.data, message="Member details retrieved successfully") + except MemberProfile.DoesNotExist: + return api_response(message="Member not found", status_code=404) diff --git a/apps/core/views/user_views.py b/apps/core/views/user_views.py new file mode 100644 index 0000000..8e3dda8 --- /dev/null +++ b/apps/core/views/user_views.py @@ -0,0 +1,403 @@ +# apps/core/user_views.py +import os + +import requests +from knox.models import AuthToken +from rest_framework.decorators import api_view, permission_classes, action +from rest_framework.generics import get_object_or_404 +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.views import APIView +from rest_framework import status, viewsets +from django.db import transaction + +from apps.company.models import CompanyProfile +from apps.core.serializers.user_serializers import CustomUserSerializer, RegisterSerializer, BaseUserSerializer +from apps.core.serializers.profile_serializers import UserProfileSerializer, UpdateProfileAccountDetailsSerializer +from apps.core.serializers.talent_serializers import TalentProfileSerializer +from apps.company.serializers import CompanyProfileSerializer +from apps.core.models import UserProfile +from apps.member.models import MemberProfile +from apps.mentorship.models import MentorshipProgramProfile, MentorProfile, MenteeProfile, MentorRoster +from apps.mentorship.serializer import MentorshipProgramProfileSerializer, MentorRosterSerializer +from utils.company_utils import create_or_update_company_connection +from utils.data_utils import extract_user_data, extract_company_data, extract_profile_data, extract_talent_data, \ + extract_profile_id_data +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.cache_utils import cache_decorator +from utils.api_helpers import api_response +from utils.profile_utils import get_user_profile, update_user_profile +from utils.emails import send_dynamic_email +from utils.slack import fetch_new_posts, send_invite + +logger = get_logger(__name__) + + +class UserDataView(APIView): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + @cache_decorator(timeout=300) # Cache for 5 minutes + def get(self, request): + user = request.user + try: + user_profile = get_user_profile(user.id) + user_data = CustomUserSerializer(user).data + profile_data = UserProfileSerializer(user_profile).data + + response_data = { + "user_info": { + **user_data, + "userprofile": profile_data, + }, + "account_info": {field: getattr(user, field) for field in [ + "is_staff", "is_recruiter", "is_member", "is_member_onboarding_complete", + "is_mentor", "is_mentee", "is_mentor_profile_active", "is_open_doors", + "is_open_doors_onboarding_complete", "is_mentor_profile_removed", "is_mentor_training_complete", + "is_mentor_interviewing", "is_mentor_profile_paused", + "is_community_recruiter", "is_company_account", "is_email_confirmation_sent", + "is_email_confirmed", "is_company_onboarding_complete", + "is_mentor_profile_approved", "is_mentor_application_submitted", + "is_speaker", "is_volunteer", "is_team", "is_community_recruiter", + "is_company_account", "is_partnership", "is_company_review_access_active" + ]}, + "mentor_details": {}, + "mentee_details": {}, + "mentor_roster_data": {}, + } + + # Mentor application data + if user.is_mentor_application_submitted: + mentor_application = get_object_or_404(MentorshipProgramProfile, user=user) + response_data["mentor_data"] = MentorshipProgramProfileSerializer(mentor_application).data + + # Mentee data + if user.is_mentee: + mentee_profile = get_object_or_404(MenteeProfile, user=user) + response_data["mentee_details"] = {"id": mentee_profile.id} + mentorship_roster = MentorRoster.objects.filter(mentee=mentee_profile) + if mentorship_roster.exists(): + response_data["mentor_roster_data"] = MentorRosterSerializer(mentorship_roster, many=True).data + + # Talent profile data + member_profile = MemberProfile.objects.filter(user=user).first() + if member_profile: + response_data["user_info"]["memberprofile"] = TalentProfileSerializer(member_profile).data + + # Company account data + if user.is_company_account: + company_account_details = get_object_or_404(CompanyProfile, account_owner=user) + local_company_data = CompanyProfileSerializer(company_account_details).data + + # External API call for additional company details + company_id = company_account_details.id + full_company_details_url = f'{os.environ.get("TC_API_URL")}core/api/company/details/?company_id={company_id}' + try: + response = requests.get(full_company_details_url, timeout=5) + response.raise_for_status() + company_account_data = response.json() + company_account_data["company_profile"] = local_company_data + except requests.RequestException as e: + logger.error(f"Error fetching company details: {str(e)}") + company_account_data = {"error": "Could not fetch company details"} + + response_data["company_account_data"] = company_account_data + + return api_response(data=response_data, message="User data retrieved successfully") + + except Exception as e: + logger.error(f"Error retrieving user data for user {user.id}: {str(e)}") + return api_response(message="An error occurred while retrieving user data", status_code=500) + + +class ProfileUpdateView(APIView): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + user = request.user + serializer = UserProfileSerializer(data=request.data, partial=True) + if serializer.is_valid(): + updated_profile = update_user_profile(user.id, serializer.validated_data) + return api_response( + data=UserProfileSerializer(updated_profile).data, + message="Profile updated successfully" + ) + return api_response(errors=serializer.errors, status_code=400) + + +class UserRegistrationView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + serializer = RegisterSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + user.is_member = True + + # Create the UserProfile and MemberProfile to replate the signal file + UserProfile.objects.create(user=user) + MemberProfile.objects.create(user=user) + + try: + send_dynamic_email({ + "recipient_emails": [user.email], + "template_id": "d-342822c240ed43778ba9e94a04fb10cf", + "dynamic_template_data": { + "first_name": user.first_name, + }, + }) + user.is_email_confirmation_sent = True + except Exception as e: + user.is_email_confirmation_sent = False + logger.error(f"Failed to send welcome email: {str(e)}") + _, token = AuthToken.objects.create(user) + user.save() + return api_response(message="User created successfully", data={"token": token}, + status_code=status.HTTP_201_CREATED) + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + +# TODO | REFACTOR => MOVE TO apps.company +class CompanyRegistrationView(APIView): + permission_classes = [AllowAny] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + serializer = RegisterSerializer(data=request.data) + if serializer.is_valid(): + with transaction.atomic(): + user = serializer.save() + company_profile = CompanyProfile.objects.create( + account_creator=user, + company_name=request.data.get('company_name') + ) + company_profile.account_owner.add(user) + company_profile.hiring_team.add(user) + + try: + send_dynamic_email({ + "recipient_emails": [user.email], + "template_id": "your_welcome_email_template_id", + "dynamic_template_data": { + "first_name": user.first_name, + "company_name": company_profile.company_name, + }, + }) + except Exception as e: + logger.error(f"Failed to send welcome email: {str(e)}") + + return api_response(message="Company and user created successfully", status_code=status.HTTP_201_CREATED) + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + +class UserProfileManagementView(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_notifications(self, request): + user_profile = request.user.userprofile + fields_to_update = [ + "marketing_jobs", "marketing_events", "marketing_org_updates", + "marketing_identity_based_programing", "marketing_monthly_newsletter" + ] + for field in fields_to_update: + setattr(user_profile, field, bool(request.data.get(field))) + user_profile.save() + return api_response(message="Notification preferences updated successfully") + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_identity(self, request): + user_profile = request.user.userprofile + serializer = UserProfileSerializer(user_profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="Identity information updated successfully") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_social_accounts(self, request): + user_profile = request.user.userprofile + serializer = UserProfileSerializer(user_profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="Social accounts updated successfully") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_skills_roles(self, request): + user_profile = request.user.memberprofile + serializer = TalentProfileSerializer(user_profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="Skills and roles updated successfully") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_work_place(self, request): + user = request.user + company_data = request.data.get("select_company") + if company_data: + company = CompanyProfile.objects.get(id=company_data["id"]) + else: + company_serializer = CompanyProfileSerializer(data=request.data, context={"request": request}) + if company_serializer.is_valid(): + company = company_serializer.save() + else: + return api_response(errors=company_serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + company.current_employees.add(user) + + user_profile = user.memberprofile + serializer = TalentProfileSerializer(user_profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="Work place information updated successfully") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + @log_exception(logger) + @timed_function(logger) + @action(detail=False, methods=['POST']) + def update_account_details(self, request): + user = request.user + try: + profile = user.userprofile + except UserProfile.DoesNotExist: + return api_response(message="Profile not found", status_code=status.HTTP_404_NOT_FOUND) + + serializer = UpdateProfileAccountDetailsSerializer(profile, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return api_response(message="Account details updated successfully") + return api_response(errors=serializer.errors, status_code=status.HTTP_400_BAD_REQUEST) + + +class MemberCreationView(APIView): + parser_classes = [MultiPartParser] + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + """ + Create a new member profile for an existing user. + + This view handles the creation of a new member profile, including associated + user profile, talent profile, and company connection. It also handles mentorship + program creation if applicable. + + Returns: + Response: A JSON response indicating success or failure of the operation. + + Raises: + ValidationError: If the data provided is invalid. + IntegrityError: If there's a database integrity error. + """ + if request.user.is_member_onboarding_complete: + return api_response( + message="Member has already been created for this user.", + status_code=status.HTTP_400_BAD_REQUEST + ) + + try: + data = request.data + user_data = extract_user_data(data) + company_data = extract_company_data(data) + profile_data = extract_profile_id_data(data, request.FILES) + talent_data = extract_profile_id_data(data, request.FILES) + + with transaction.atomic(): + # Update user + user_serializer = CustomUserSerializer(request.user, data=user_data, partial=True) + user_serializer.is_valid(raise_exception=True) + user = user_serializer.save() + + # Update talent profile + talent_serializer = TalentProfileSerializer(user.user, data=talent_data, partial=True) + talent_serializer.is_valid(raise_exception=True) + talent_profile = talent_serializer.save() + + # Update user profile + profile_serializer = UserProfileSerializer(user.userprofile, data=profile_data, partial=True) + profile_serializer.is_valid(raise_exception=True) + user_profile = profile_serializer.save() + + # Create or update company connection + user_company_connection = create_or_update_company_connection(user, company_data) + + # Handle mentorship program + if user_data.get("is_mentee") or user_data.get("is_mentor"): + mentorship_program = MentorshipProgramProfile.objects.create(user=user) + user.is_mentee = user_data.get("is_mentee", False) + user.is_mentor = user_data.get("is_mentor", False) + if user_data.get("is_mentor"): + mentor_profile = MentorProfile.objects.create(user=user) + mentorship_program.mentor_profile = mentor_profile + mentorship_program.save() + + # Send welcome email + email_data = { + "recipient_emails": [user.email], + "template_id": "d-96a6752bd6b74888aa1450ea30f33a06", + "dynamic_template_data": {"first_name": user.first_name}, + } + send_dynamic_email(email_data) + + user.is_member_onboarding_complete = True + user.is_company_review_access_active = True + user.save() + + # Send Slack invite + try: + send_invite(user.email) + user.is_slack_invite_sent = True + user.save() + except Exception as e: + logger.error(f"Failed to send Slack invite for user {user.id}: {str(e)}") + user.is_slack_invite_sent = False + user.save() + + return api_response( + message="User, MemberProfile, and UserProfile created successfully!", + status_code=status.HTTP_200_OK + ) + + except Exception as e: + logger.error(f"Error creating new member: {str(e)}") + return api_response( + message="An error occurred while creating the member profile.", + errors=str(e), + status_code=status.HTTP_400_BAD_REQUEST + ) + + +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +@log_exception(logger) +@timed_function(logger) +def get_announcement(request): + try: + slack_msg = fetch_new_posts("CELK4L5FW", 1) + if slack_msg: + return api_response(data={"announcement": slack_msg}, message="Announcement retrieved successfully") + else: + return api_response(message="No new messages.", status_code=status.HTTP_404_NOT_FOUND) + except Exception as e: + logger.error(f"Error pulling slack message: {str(e)}") + return api_response(message="An error occurred while fetching the announcement", + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) diff --git a/apps/core/views_admin.py b/apps/core/views_admin.py deleted file mode 100644 index 41886b7..0000000 --- a/apps/core/views_admin.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.db.models import Count -from rest_framework.permissions import IsAdminUser -from rest_framework.views import APIView -from rest_framework.response import Response - -from .models import CustomUser, SexualIdentities, GenderIdentities, EthicIdentities, PronounsIdentities -from ..company.models import Department, Roles, Industries, Skill, CompanyProfile -from ..member.models import MemberProfile -from ..mentorship.models import MentorProfile - - -class CombinedBreakdownView(APIView): - permission_classes = [IsAdminUser] - - def get(self, request, *args, **kwargs): - # Aggregate MemberProfile by tech_journey - tech_journey_counts = MemberProfile.objects.values('tech_journey').annotate( - count=Count('tech_journey')).order_by('tech_journey') - # Map codes to labels - for item in tech_journey_counts: - item['name'] = dict(MemberProfile.CAREER_JOURNEY).get(item['tech_journey'], 'Unknown') - - response_data = { - 'skills': list( - Skill.objects.annotate(members_count=Count('talent_skills_list')).filter(members_count__gt=0).order_by( - '-members_count').values('name', 'members_count')), - 'departments': list(Department.objects.annotate(members_count=Count('talent_department_list')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'roles': list( - Roles.objects.annotate(members_count=Count('talent_role_types')).filter(members_count__gt=0).order_by( - '-members_count').values('name', 'members_count')), - 'industries': list(Industries.objects.annotate(members_count=Count('member_industries')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'identity_sexuality': list( - SexualIdentities.objects.annotate(members_count=Count('userprofile_identity_sexuality')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'identity_gender': list( - GenderIdentities.objects.annotate(members_count=Count('userprofile_identity_gender')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'identity_ethic': list( - EthicIdentities.objects.annotate(members_count=Count('userprofile_identity_ethic')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'identity_pronouns': list( - PronounsIdentities.objects.annotate(members_count=Count('userprofile_identity_pronouns')).filter( - members_count__gt=0).order_by('-members_count').values('name', 'members_count')), - 'total_member': CustomUser.objects.filter(is_member=True).count(), - 'total_member_level': tech_journey_counts, - 'total_member_talent_choice': CustomUser.objects.filter(is_talent_choice=True).count(), - 'talent_choice_job_roles_needed': list( - Roles.objects.filter( - talent_role_types__user__is_talent_choice=True - ).annotate(members_count=Count('talent_role_types')).filter(members_count__gt=0).order_by( - '-members_count').values('name', 'members_count')), - 'total_company_talent_choice': CompanyProfile.objects.filter(talent_choice_account=True).count(), - 'total_active_mentors': MentorProfile.objects.filter(mentor_status__exact="active").count(), - 'total_mentors_applications': MentorProfile.objects.filter(mentor_status__exact="submitted").count(), - 'total_mentors_interviewing': MentorProfile.objects.filter(mentor_status__exact="interviewing").count(), - 'total_mentors_need_cal_info': MentorProfile.objects.filter(mentor_status__exact="need_cal_info").count(), - } - - # DEI Categories might require custom handling, you may need to adjust the function or directly query here - - return Response(response_data) - - -def get_aggregated_data(model, related_name=None): - if related_name: - return model.objects.annotate(user_count=Count(related_name)).all() - else: - # For models without a direct relation, adjust accordingly - return model.objects.all() diff --git a/apps/core/views_auth.py b/apps/core/views_auth.py deleted file mode 100644 index defead8..0000000 --- a/apps/core/views_auth.py +++ /dev/null @@ -1,83 +0,0 @@ -import os - -from django.contrib.auth.tokens import default_token_generator -from django.core.mail import send_mail, EmailMessage -from django.template.loader import render_to_string -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode -from rest_framework import permissions, views, response, status -from rest_framework.permissions import AllowAny -from rest_framework.response import Response -from rest_framework.views import APIView - -from apps.core.models import CustomUser -from apps.core.serialiizers.password_reset import PasswordResetSerializer, SetNewPasswordSerializer - - -class UserPermissionAPIView(views.APIView): - permission_classes = [permissions.IsAuthenticated] - - def get(self, request): - permissions = request.user.is_authenticated - return response.Response({'permissions': permissions}) - - -class PasswordResetRequestView(APIView): - permission_classes = [AllowAny] - serializer_class = PasswordResetSerializer - - def post(self, request): - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - user = CustomUser.objects.get(email=serializer.validated_data['email']) - uidb64 = urlsafe_base64_encode(force_bytes(user.pk)) - token = default_token_generator.make_token(user) - reset_link = f"{os.getenv('FRONTEND_URL')}password-reset/confirm-password/{uidb64}/{token}" - email_data = { - 'username': user.first_name, - 'reset_link': reset_link - } - - send_password_email(user.email, user.first_name, user, reset_link) - - return Response({"message": "Password reset link sent."}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class PasswordResetConfirmView(APIView): - permission_classes = [AllowAny] - serializer_class = SetNewPasswordSerializer - - def post(self, request, uidb64, token): - serializer = self.serializer_class(data=request.data, context={'uidb64': uidb64, 'token': token}) - t = serializer.is_valid() - print(t) - if serializer.is_valid(): - return Response({"status": True, "message": "Password has been reset."}, status=status.HTTP_200_OK) - return Response({"status": False, "message": "Token not valid"}, status=status.HTTP_400_BAD_REQUEST) - - -def send_password_email(email, first_name, user, reset_link): - """ - Send a password reset email to the user. - """ - mail_subject = 'Password Reset Request' - - context = { - 'username': first_name, - 'reset_link': reset_link, - } - - message = render_to_string('emails/password_reset_email.txt', context=context) - email_msg = EmailMessage(mail_subject, message, 'no-reply@yourdomain.com', [email]) - email_msg.extra_headers = { - 'email_template': 'emails/password_reset_email.html', - 'username': first_name, - 'reset_link': reset_link, - } - - try: - email_msg.send() - except Exception as e: - print("Error while sending email: ", str(e)) - diff --git a/apps/core/views_core.py b/apps/core/views_core.py deleted file mode 100644 index b16f8e6..0000000 --- a/apps/core/views_core.py +++ /dev/null @@ -1,121 +0,0 @@ -from rest_framework.decorators import api_view -from rest_framework.response import Response - -from apps.company.models import ( - Roles, - CompanyProfile, - CAREER_JOURNEY, - Skill, - Department, - Industries, - CompanyTypes, - SalaryRange, COMPANY_SIZE, ON_SITE_REMOTE, -) -from apps.core.models import ( - PronounsIdentities, - GenderIdentities, - SexualIdentities, - EthicIdentities, - CommunityNeeds, - UserProfile, -) -from apps.core.serializers_member import TalentProfileSerializer, FullTalentProfileSerializer -from apps.member.models import MemberProfile -from utils.helper import CustomPagination, paginate_items - - -@api_view(["GET"]) -def get_dropdown_data(request): - data = {} - requested_fields = request.query_params.getlist("fields", []) - - if not requested_fields or "pronouns" in requested_fields: - data["pronouns"] = list(PronounsIdentities.objects.values("name", "id")) - - if not requested_fields or "job_roles" in requested_fields: - data["job_roles"] = list(Roles.objects.order_by("name").values("name", "id")) - - if not requested_fields or "years_of_experience" in requested_fields: - data["years_of_experience"] = [ - {"value": code, "label": description} - for code, description in CAREER_JOURNEY - ] - - if not requested_fields or "companies" in requested_fields: - data["companies"] = list( - CompanyProfile.objects.order_by("company_name").values( - "company_name", "id", "logo", "company_url" - ) - ) - - if not requested_fields or "job_skills" in requested_fields: - data["job_skills"] = list(Skill.objects.order_by("name").values("name", "id")) - - if not requested_fields or "job_departments" in requested_fields: - data["job_departments"] = list( - Department.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "job_industries" in requested_fields: - data["job_industries"] = list( - Industries.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "company_types" in requested_fields: - data["company_types"] = list( - CompanyTypes.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "gender" in requested_fields: - data["gender"] = list( - GenderIdentities.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "sexuality" in requested_fields: - data["sexuality"] = list( - SexualIdentities.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "ethic" in requested_fields: - data["ethic"] = list( - EthicIdentities.objects.order_by("name").values("name", "id") - ) - - if not requested_fields or "job_salary_range" in requested_fields: - data["job_salary_range"] = list(SalaryRange.objects.values("range", "id")) - - if not requested_fields or "community_needs" in requested_fields: - data["community_needs"] = list(CommunityNeeds.objects.values("name", "id")) - - if not requested_fields or "how_connected" in requested_fields: - data["how_connected"] = UserProfile.HOW_CONNECTION_MADE - - if not requested_fields or "company_size" in requested_fields: - data["company_size"] = COMPANY_SIZE - - if not requested_fields or "on_site_remote" in requested_fields: - data["on_site_remote"] = ON_SITE_REMOTE - - data["status"] = True - - return Response(data) - - -@api_view(["GET"]) -def get_all_members(request): - data = {} - - # Initialize the paginator - paginator = CustomPagination() - - requested_fields = request.query_params.getlist("fields", []) - - members = MemberProfile.objects.filter(user__is_active=True).order_by("created_at") - paginated_members = paginate_items(members, request, paginator, FullTalentProfileSerializer) - - data["members"] = paginated_members - data["status"] = True - - return Response(data) - - diff --git a/apps/core/views_internal.py b/apps/core/views_internal.py deleted file mode 100644 index 1372ee8..0000000 --- a/apps/core/views_internal.py +++ /dev/null @@ -1,73 +0,0 @@ -# views.py -from django.db.models import F -from rest_framework import permissions, status -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework.views import APIView -from .models import UserProfile # Assuming you have a UserProfile model - - -class ExternalView(APIView): - # permission_classes = [permissions.IsAuthenticated] - - def get(self, request): - user = request.user - user_account_data = {} - user_data = {} - - try: - user_demo_data = UserProfile.objects.filter(user=user).annotate( - sexuality_name=F('identity_sexuality__name'), - gender_name=F('identity_gender__name'), - ethic_name=F('identity_ethic__name'), - pronouns_name=F('identity_pronouns__name'), - ).values( - 'sexuality_name', - 'gender_name', - 'ethic_name', - 'pronouns_name', - "disability", - "care_giver", - "veteran_status", - ) - - user_account_data = { - "is_company_review_access_active": user.is_company_review_access_active, - "company_review_tokens": user.company_review_tokens, - } - - user_data = { - "user_demo": list(user_demo_data), - "user_account": user_account_data, - "user_id": request.user.id - } - return Response(user_data, status=status.HTTP_200_OK) - - except UserProfile.DoesNotExist: - print(f"UserProfile does not exist for user {user.id}") - return Response( - {"error": "UserProfile not found"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - print(f"Unexpected error retrieving user data for {user.id}: {e}") - return Response( - {"error": "An unexpected error occurred"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - @api_view(["POST"]) - def update_review_token_total(request): - # update/review-token/ - user = request.user - direction = request.data.get('direction', True) - - token_change = 1 if direction else -1 - user.company_review_tokens += token_change - - if user.company_review_tokens == 0: - user.is_company_review_access_active = False - - user.save() - - return Response({"data": user.company_review_tokens}, status=status.HTTP_200_OK) - diff --git a/apps/core/views_member.py b/apps/core/views_member.py deleted file mode 100644 index 7e4ecc8..0000000 --- a/apps/core/views_member.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.core.serializers import serialize -from django.http import Http404 -from rest_framework import status -from rest_framework.decorators import permission_classes -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.views import APIView - -from apps.company.models import CompanyProfile -from apps.core.models import CustomUser, UserProfile -from apps.core.serializers import TalentProfileSerializer -from apps.core.serializers_member import CustomUserSerializer, UserProfileSerializer, ReadOnlyCustomUserSerializer, \ - ReadOnlyUserProfileSerializer, ReadOnlyTalentProfileSerializer -from apps.core.util import get_current_company_data -from apps.mentorship.models import ( - MentorProfile, - MenteeProfile, - MentorshipProgramProfile, -) -from apps.mentorship.serializer import ( - MentorshipProgramProfileSerializer, -) -from apps.member.models import MemberProfile - - -class MemberDetailsView(APIView): - """ - Retrieve CustomUser, UserProfile, and MemberProfile details. - """ - - permission_classes = [IsAuthenticated] - - # # @permission_classes([IsAuthenticated]) - def get_profile(self, model, user): - try: - return model.objects.get(user=user) - except model.DoesNotExist: - return None - - # @permission_classes([IsAuthenticated]) - def get(self, request, pk, format=None): - try: - user = CustomUser.objects.get(pk=pk) - except CustomUser.DoesNotExist: - raise Http404("Member not found.") - - user_profile = self.get_profile(UserProfile, user) - if not user_profile: - raise Http404("User profile not found.") - - talent_profile = self.get_profile(MemberProfile, user) - if not talent_profile: - raise Http404("Talent profile not found.") - - mentor_program = self.get_profile(MentorshipProgramProfile, user) - current_company_data = get_current_company_data(user) - - user_serializer = ReadOnlyCustomUserSerializer(user) - user_profile_serializer = ReadOnlyUserProfileSerializer(user_profile) - talent_profile_serializer = ReadOnlyTalentProfileSerializer(talent_profile) - mentor_program_serializer = MentorshipProgramProfileSerializer(mentor_program).data - - data = { - "user": user_serializer.data, - "user_profile": user_profile_serializer.data, - "talent_profile": talent_profile_serializer.data, - "current_company": current_company_data, - "mentorship_program": mentor_program_serializer - } - - return Response( - { - "success": True, - "message": "Member details retrieved successfully.", - "data": data, - }, - status=status.HTTP_200_OK, - ) diff --git a/apps/core/views_open_doors.py b/apps/core/views_open_doors.py deleted file mode 100644 index 420660c..0000000 --- a/apps/core/views_open_doors.py +++ /dev/null @@ -1,155 +0,0 @@ -import os - -import requests -from django.http import JsonResponse -from knox.models import AuthToken -from rest_framework.decorators import action -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.response import Response -from rest_framework import status, viewsets -from django.core.mail import send_mail -from django.urls import reverse -import uuid - -from rest_framework.viewsets import ViewSet - -from .models import UserVerificationToken -from .serializers import UserProfileSerializer -from django.contrib.auth import get_user_model - -from .serializers_open_doors import UserRegistrationSerializer -from .views import send_welcome_email - -User = get_user_model() - - -class UserManagementView(ViewSet): - - def get_permissions(self): - """ - Instantiates and returns the list of permissions that this view requires. - """ - if self.action == 'create': - permission_classes = [AllowAny] - else: - permission_classes = [IsAuthenticated] - return [permission() for permission in permission_classes] - - def create(self, request, *args, **kwargs): - """ - Create a new Open Doors user. This view handles the POST request to register a new user. - It performs input validation, user creation, and sending a welcome email. - """ - if request.method != "POST": - return JsonResponse( - {"status": False, "error": "Invalid request method"}, status=405 - ) - # This assumes the action is for registration. - serializer = UserRegistrationSerializer(data=request.data) - if serializer.is_valid(): - user = serializer.save() - user.is_open_doors = True - user.is_company_review_access_active = True - user.save() - _, token = AuthToken.objects.create(user) - verification_token = str(uuid.uuid4()) - UserVerificationToken.objects.create(user=user, token=verification_token) - frontend_url = os.getenv("FRONTEND_URL", "default_fallback_url") - verification_url = f"{frontend_url}?token={verification_token}" - send_welcome_email(user.email, user.first_name, company_name='reviewSite', user=user, - current_site=frontend_url, request=request) - return Response({'status': True, - "token": token, - 'message': 'User registered. Please check your email.'}, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=False, methods=['post'], url_path='confirm-agreement') - def service_agreement(self, request): - if request.data.get('confirm_service_agreement'): - user = request.user - user.confirm_service_agreement = True - user.is_open_doors_onboarding_complete = True - user.is_company_review_access_active = True - user.save() - - return Response({ - "status": True, - "message": "Welcome to Open Doors." - }, status=status.HTTP_200_OK) - else: - return Response({ - "status": False, - "message": "Please accept the service agreement to create your account." - }, status=status.HTTP_200_OK) - - def patch(self, request, *args, **kwargs): - # This assumes the action is for updating user demographics. - user = request.user # Assuming the user is authenticated - serializer = UserProfileSerializer(user.userprofile, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - return Response({'message': 'User profile updated successfully.'}, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @action(detail=False, methods=['post'], url_path='submit-report') - def post_review_submission(self, request): - """ - Sends item ID and form data to a 3rd party API. - - :param request: The request object containing form data. - :param pk: Primary key of the item to be processed, taken from the URL. - :return: A Response object with the result of the 3rd party API interaction. - """ - user_id = request.user.id - # Create a mutable copy of request.data - header_token = request.headers.get("Authorization", None) - mutable_data = request.data.copy() - mutable_data['user_id'] = user_id - mutable_data['header_token'] = header_token - third_party_url = f'{os.getenv("OD_API_URL")}reports/submit-report/' - - # Make the 3rd party API request - try: - headers = { - "Content-Type": "application/json", - "Authorization": header_token - } - response = requests.post(third_party_url, json=mutable_data, headers=headers) - response.raise_for_status() - # Process the response from the 3rd party API - result_data = response.json() - return Response(result_data, status=status.HTTP_200_OK) - except requests.RequestException as e: - return Response({"status": False, "message": "No data saved"}) - - return Response(data={"status": True}, status=status.HTTP_201_CREATED) - - @action(detail=True, methods=['get'], url_path='get-report') - def get_review_submission(self, request, pk=None): - """ - Submits the the review - - :param request: The request object containing form data. - :param pk: Primary key of the item to be processed, taken from the URL. - :return: A Response object with the result of the 3rd party API interaction. - """ - user_id = request.user.id - # # Create a mutable copy of request.data - header_token = request.headers.get("Authorization", None) - data = {"id": user_id} - # mutable_data['user_id'] = user_id - # mutable_data['header_token'] = header_token - third_party_url = f'{os.getenv("OD_API_URL")}reports/get/report/{pk}/' - # - # # Make the 3rd party API request - try: - response = requests.get(third_party_url, data=data) - response.raise_for_status() - # Process the response from the 3rd party API - result_data = response.json() - return Response(result_data, status=status.HTTP_200_OK) - except requests.RequestException as e: - return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - # - # return Response(data={"status": True}, status=status.HTTP_201_CREATED) - return Response({"message": "Here is the report for ID: {}".format(pk)}) diff --git a/apps/core/views_talent_choice.py b/apps/core/views_talent_choice.py deleted file mode 100644 index 4da7f22..0000000 --- a/apps/core/views_talent_choice.py +++ /dev/null @@ -1,197 +0,0 @@ -import json -import logging -import os - -import requests -from django.contrib.auth.tokens import default_token_generator -from django.utils.encoding import force_str -from django.utils.http import urlsafe_base64_decode -from django.views.decorators.csrf import csrf_exempt -from knox.models import AuthToken -from rest_framework import viewsets, status -from rest_framework.generics import get_object_or_404 -from rest_framework.decorators import action, permission_classes -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.response import Response -from rest_framework.views import APIView -from rest_framework.viewsets import ViewSet - -from utils.helper import prepend_https_if_not_empty -from .models import CustomUser -from ..company.models import CompanyProfile - -logger = logging.getLogger(__name__) - - -class CompanyViewSet(ViewSet): - - @action(detail=True, methods=['post'], url_path='complete-onboarding') - def complete_onboarding(self, request): - company_profile = CompanyProfile.objects.get(account_creator=request.user) - company_id = company_profile.id - - user_data = request.user - - # Prepare the data to include the token - data_to_send = request.data.copy() - if request.auth: - data_to_send['token'] = str(request.auth) - data_to_send['companyId'] = company_id - - try: - response = requests.post( - f'{os.environ["TC_API_URL"]}company/new/onboarding/open-roles/', - data=json.dumps(data_to_send), - headers={'Content-Type': 'application/json'}, - verify=True - ) - response.raise_for_status() - talent_choice_jobs = response.json() - except requests.exceptions.HTTPError as http_err: - return Response( - {"status": False, "error": f"HTTP error occurred: {http_err}"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - - # Update user data to indicate onboarding completion - user_data.is_company_onboarding_complete = True - user_data.save() - - return Response({ - "status": True, - "message": "Welcome to Talent Choice." - }, status=status.HTTP_200_OK) - - @csrf_exempt - @action(detail=False, methods=['post'], url_path='service-agreement') - def service_agreement(self, request): - if request.data.get('confirm_service_agreement'): - user = request.user - company_profile = CompanyProfile.objects.get(account_creator=user) - token = request.headers.get('Authorization', None) - if token: - clean_token = token.split()[1] - else: - clean_token = request.auth - print(clean_token) - - try: - response = requests.post(f'{os.environ["TC_API_URL"]}company/new/onboarding/confirm-terms/', - data={'companyId': company_profile.id, 'token': clean_token}, verify=True) - response.raise_for_status() - talent_choice_jobs = response.json() - except requests.exceptions.HTTPError as http_err: - return Response( - {"status": False, "error": f"HTTP error occurred: {http_err}"}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR - ) - company_profile.confirm_service_agreement = True - company_profile.save() - - return Response({ - "status": True, - "message": "Welcome to Talent Choice." - }, status=status.HTTP_200_OK) - else: - return Response({ - "status": False, - "message": "Please accept the service agreement to create your account." - }, status=status.HTTP_200_OK) - - @action(detail=False, methods=['post'], url_path='create-onboarding') - def create_onboarding(self, request): - if request.data: - user = request.user - company_profile = CompanyProfile.objects.get(account_creator=user) - - # Access form fields - company_url = prepend_https_if_not_empty(request.data.get('website', '')) - linkedin = prepend_https_if_not_empty(request.data.get('linkedin', '')) - twitter = prepend_https_if_not_empty(request.data.get('twitter', '')) - youtube = prepend_https_if_not_empty(request.data.get('youtube', '')) - facebook = prepend_https_if_not_empty(request.data.get('facebook', '')) - instagram = prepend_https_if_not_empty(request.data.get('instagram', '')) - - location = request.data.get('location', '') - city = request.data.get('city', '') - state = request.data.get('state', '') - postal_code = request.data.get('postalCode', '') - mission = request.data.get('mission') - # For single value fields like autocomplete where the value is expected as a string - company_size = request.data.get('company_size') - # company_types = request.data.get('company_types') - - # For the file upload, the 'logo' field should be a file type - logo = request.FILES.get('logo') - if logo: - company_profile.logo = logo - - company_profile.company_url = company_url - company_profile.linkedin = linkedin - company_profile.twitter = twitter - company_profile.youtube = youtube - company_profile.facebook = facebook - company_profile.instagram = instagram - company_profile.location = location - company_profile.state = state - company_profile.city = city - company_profile.postal_code = postal_code - company_profile.mission = mission - company_profile.company_size = company_size - # company_profile.company_types.set(company_types) - # company_profile.industries.set(request.data.get('company_industries')) - company_profile.save() - return Response({'status': True, "companyId": company_profile.id}, status=status.HTTP_201_CREATED) - else: - return Response({ - "status": False, - "message": "Issue saving account data", - }, status=status.HTTP_400_BAD_REQUEST) - - -class ConfirmEmailAPIView(APIView): - permission_classes = [AllowAny] - def get(self, request, id=None, token=None): - print("starting email confirmation flow") - try: - # Decode the user ID - uid = force_str(urlsafe_base64_decode(id)) - user = get_object_or_404(CustomUser, id=uid) - - if user.is_email_confirmed: - return Response({ - "status": True, - "message": "Email already confirmed!" - }, status=status.HTTP_208_ALREADY_REPORTED) - # Validate the token - is_token_valid = default_token_generator.check_token(user, token) - - if is_token_valid: - - # Confirm the email - user.is_active = True - user.is_email_confirmed = True - user.save() - - _, token = AuthToken.objects.create(user) - - return Response({ - "status": True, - "token": token, - "message": "Email confirmed! Please complete your account." - }, status=status.HTTP_200_OK) - else: - return Response({"status": False, "message": "Invalid email token. Please contact support."}, - status=status.HTTP_400_BAD_REQUEST) - - except CustomUser.DoesNotExist: - logger.warning(f"User does not exist for id: {id}") - return Response({"status": False, "message": "Invalid email token or user does not exist."}, - status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.error(f"Unhandled error: {e}") - return Response({"status": False, "message": "An unexpected error occurred."}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # except jwt.DecodeError: - # return Response({"status": False, "error": "Invalid token."}, status=status.HTTP_400_BAD_REQUEST) diff --git a/apps/member/urls.py b/apps/member/urls.py new file mode 100644 index 0000000..82c1235 --- /dev/null +++ b/apps/member/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from apps.member.views.member_views import MemberCreationView + +urlpatterns = [ + path('create-member/', MemberCreationView.as_view(), name='create-new-member'), +] diff --git a/apps/member/views.py b/apps/member/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/apps/member/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/apps/member/views/__init__.py b/apps/member/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/member/views/member_views.py b/apps/member/views/member_views.py new file mode 100644 index 0000000..6e6484d --- /dev/null +++ b/apps/member/views/member_views.py @@ -0,0 +1,120 @@ +import logging +from rest_framework import status +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from django.db import transaction + +from apps.core.models import CustomUser +from apps.core.serializers.user_serializers import CustomUserSerializer +from apps.core.serializers.profile_serializers import UserProfileSerializer +from apps.core.serializers.talent_serializers import TalentProfileSerializer +from apps.mentorship.models import MentorshipProgramProfile, MentorProfile +from utils.logging_helper import get_logger, log_exception, timed_function +from utils.data_utils import extract_user_data, extract_company_data, extract_profile_data, extract_talent_data +from utils.profile_utils import update_user_profile, update_talent_profile +from utils.company_utils import create_or_update_company_connection +from utils.emails import send_dynamic_email +from utils.slack import send_invite +from utils.api_helpers import api_response + +logger = get_logger(__name__) + + +class MemberCreationView(APIView): + parser_classes = [MultiPartParser] + permission_classes = [IsAuthenticated] + + @log_exception(logger) + @timed_function(logger) + def post(self, request): + """ + Create a new member profile for an existing user. + + This view handles the creation of a new member profile, including associated + user profile, talent profile, and company connection. It also handles mentorship + program creation if applicable. + + Returns: + Response: A JSON response indicating success or failure of the operation. + + Raises: + ValidationError: If the data provided is invalid. + IntegrityError: If there's a database integrity error. + """ + if request.user.is_member_onboarding_complete: + return api_response( + message="Member has already been created for this user.", + status_code=status.HTTP_400_BAD_REQUEST + ) + + try: + data = request.data + user_data = extract_user_data(data) + company_data = extract_company_data(data) + profile_data = extract_profile_data(data, request.FILES) + talent_data = extract_talent_data(data, request.FILES) + + with transaction.atomic(): + # Update user + user_serializer = CustomUserSerializer(request.user, data=user_data, partial=True) + user_serializer.is_valid(raise_exception=True) + user = user_serializer.save() + + # Update talent profile + talent_serializer = TalentProfileSerializer(user.memberprofile, data=talent_data, partial=True) + talent_serializer.is_valid(raise_exception=True) + talent_profile = talent_serializer.save() + + # Update user profile + profile_serializer = UserProfileSerializer(user.userprofile, data=profile_data, partial=True) + profile_serializer.is_valid(raise_exception=True) + user_profile = profile_serializer.save() + + # Create or update company connection + user_company_connection = create_or_update_company_connection(user, company_data) + + # Handle mentorship program + if user_data.get("is_mentee") or user_data.get("is_mentor"): + mentorship_program = MentorshipProgramProfile.objects.create(user=user) + user.is_mentee = user_data.get("is_mentee", False) + user.is_mentor = user_data.get("is_mentor", False) + if user_data.get("is_mentor"): + mentor_profile = MentorProfile.objects.create(user=user) + mentorship_program.mentor_profile = mentor_profile + mentorship_program.save() + + # Send welcome email + email_data = { + "recipient_emails": [user.email], + "template_id": "d-96a6752bd6b74888aa1450ea30f33a06", + "dynamic_template_data": {"first_name": user.first_name}, + } + send_dynamic_email(email_data) + + user.is_member_onboarding_complete = True + user.is_company_review_access_active = True + user.save() + + # Send Slack invite + try: + send_invite(user.email) + user.is_slack_invite_sent = True + user.save() + except Exception as e: + logger.error(f"Failed to send Slack invite for user {user.id}: {str(e)}") + user.is_slack_invite_sent = False + user.save() + + return api_response( + message="User, MemberProfile, and UserProfile created successfully!", + status_code=status.HTTP_200_OK + ) + + except Exception as e: + logger.error(f"Error creating new member: {str(e)}") + return api_response( + message="An error occurred while creating the member profile.", + errors=str(e), + status_code=status.HTTP_400_BAD_REQUEST + ) diff --git a/utils/cache_utils.py b/utils/cache_utils.py index e3ab92f..7cc9537 100644 --- a/utils/cache_utils.py +++ b/utils/cache_utils.py @@ -1,4 +1,5 @@ import logging +import time from functools import wraps from django.core.cache import cache from django.conf import settings diff --git a/utils/company_utils.py b/utils/company_utils.py new file mode 100644 index 0000000..f730f33 --- /dev/null +++ b/utils/company_utils.py @@ -0,0 +1,48 @@ +from apps.company.models import CompanyProfile + + +def create_or_update_company_connection(user, company_data): + """ + Create or update a company connection for a user. + + This function checks if the company is already in the database. If so, it adds the user + to the company's current employees. If the company is not in the database, it creates a + new company with the user as the unclaimed_account_creator and adds them to current_employees. + It also moves the user from current to past employees in their previous company, if applicable. + + Parameters: + - user (CustomUser): The user to add to the company. + - company_data (dict): A dictionary containing company_id, company_name, company_url, and company_logo. + + Returns: + - CompanyProfile: The company profile object that the user was added to. + """ + company_id = company_data.get("company_id") + company_name = company_data.get("company_name") + company_url = company_data.get("company_url") + company_logo = company_data.get("company_logo") + + # Check if the user is currently associated with a different company + previous_company = CompanyProfile.objects.filter(current_employees=user).first() + if previous_company: + previous_company.current_employees.remove(user) + previous_company.past_employees.add(user) + + if company_id: + # The company already exists, just add the user to current_employees + company_profile = CompanyProfile.objects.get(id=company_id) + company_profile.current_employees.add(user) + else: + # The company doesn't exist, create a new one and set user as unclaimed_account_creator and in current_employees + if company_name and company_url: + company_profile = CompanyProfile.objects.create( + unclaimed_account_creator=user, + is_unclaimed_account=True, + company_name=company_name, + # logo=company_logo, + company_url=company_url, + ) + company_profile.current_employees.add(user) + return company_profile + else: + return False diff --git a/utils/data_utils.py b/utils/data_utils.py index 2fee15c..269976f 100644 --- a/utils/data_utils.py +++ b/utils/data_utils.py @@ -5,6 +5,14 @@ from typing import Any, Dict, List, Union from decimal import Decimal +from django.core.exceptions import ValidationError +from rest_framework import serializers + +from apps.company.models import CompanyProfile, SalaryRange, Skill, Department, Roles, CompanyTypes +from apps.core.models import UserProfile, PronounsIdentities, EthicIdentities, GenderIdentities, SexualIdentities +from apps.core.serializers import UserProfileSerializer +from apps.member.models import MemberProfile +from .errors import CustomException # Import the custom logging utilities from .logging_helper import get_logger, log_exception, timed_function, sanitize_log_data @@ -295,3 +303,692 @@ def convert_currency(amount: Union[int, float, Decimal], from_currency: str, to_ rate = exchange_rates[from_currency][to_currency] converted_amount = Decimal(str(amount)) * rate return converted_amount.quantize(Decimal('0.01')) + + +def get_id_or_create(model, name): + obj, created = model.objects.get_or_create(name=name) + return obj.id + + +def update_user(current_user, user_data): + """ + Create or update a user instance based on the provided user_data. + + :param current_user: The user instance to update. + :param user_data: A dictionary containing the data for the user. + :return: The created or updated user instance. + """ + try: + # Assuming you're partially updating an existing user + user_serializer = UserProfileSerializer( + instance=current_user, data=user_data, partial=True + ) + + # Validate the user data + user_serializer.is_valid(raise_exception=True) + + # Save the user object and return it + return user_serializer.save() + except serializers.ValidationError as e: + # Handle validation errors, e.g., return a meaningful error message or raise an exception + print(f"Validation error while creating/updating user: {e}") + raise + except Exception as e: + # Handle unexpected errors + print(f"Unexpected error while creating/updating user: {e}") + raise + + +def update_user_profile(user, profile_data): + """ + Update a user profile. + + This function will create a new UserProfile or update an existing one based on the provided user. It will set fields + for identity_sexuality, identity_gender, identity_ethic, identity_pronouns, and other provided profile data. + + Args: + user (CustomUser): The user object for whom the profile is being created or updated. + profile_data (dict): A dictionary containing profile data. + + Returns: + UserProfile: The created or updated UserProfile object. + + Raises: + CustomException: If there's any issue in creating or updating the UserProfile or related objects. + """ + try: + user_profile = UserProfile.objects.get(user=user) + except UserProfile.DoesNotExist as e: + # Log the exception and raise a custom exception for the caller to handle + print(f"UserProfile does not exist for user {user.id}: {e}") + raise CustomException(f"Failed to create or update UserProfile: {str(e)}") + + # Process and set many-to-many fields + if "identity_sexuality" in profile_data and not profile_data[ + "identity_sexuality" + ] == [""]: + sexuality_instances = process_identity_field( + profile_data["identity_sexuality"], SexualIdentities + ) + profile_data["identity_sexuality"] = sexuality_instances + else: + del profile_data["identity_sexuality"] + + if "identity_gender" in profile_data and not profile_data["identity_gender"] == [ + "" + ]: + gender_instances = process_identity_field( + profile_data["identity_gender"], GenderIdentities + ) + profile_data["identity_gender"] = gender_instances + else: + del profile_data["identity_gender"] + + if "identity_ethic" in profile_data and not profile_data["identity_ethic"] == [""]: + ethic_instances = process_identity_field( + profile_data["identity_ethic"], EthicIdentities + ) + profile_data["identity_ethic"] = ethic_instances + else: + del profile_data["identity_ethic"] + + if "identity_pronouns" in profile_data and not profile_data[ + "identity_pronouns" + ] == [""]: + pronouns_instances = process_identity_field( + profile_data["identity_pronouns"], PronounsIdentities + ) + profile_data["identity_pronouns"] = pronouns_instances + else: + del profile_data["identity_pronouns"] + + # For fields that are not many-to-many relationships, update them directly + try: + for field, value in profile_data.items(): + if field not in [ + "identity_sexuality", + "identity_gender", + "identity_ethic", + "identity_pronouns", + ]: + setattr(user_profile, field, value) + if "photo" in field: + user_profile.photo = value + try: + user_profile.set_tbc_program_interest(profile_data["tbc_program_interest"]) + user_profile.postal_code = profile_data["postal_code"] + except Exception as e: + print(f"Error trying to update items: {e}") + try: + user_profile.save(force_update=True) + print("UserProfile saved successfully.") + except Exception as e: + print(f"Error saving UserProfile: {e}") + + # Verify the save operation by fetching the profile again + try: + updated_profile = UserProfile.objects.get(user=user) + print(f"Updated postal code: {updated_profile.postal_code}") + except UserProfile.DoesNotExist: + print("UserProfile does not exist.") + + return user_profile + except Exception as e: + print(f"Error updating user_profile: {e}") + + +def process_identity_field(identity_list, model): + """ + Process and validate name-related fields before setting them in the UserProfile. + + This function takes a list of name names or identifiers, ensures that these identities + are present in the database (creating them if necessary), and returns a queryset + or list of Identity model instances. + + Args: + identity_list (list): A list of name names or identifiers. + model (Django model class): The model class for the name (e.g., SexualIdentities, GenderIdentities). + + Returns: + QuerySet: A QuerySet of Identity instances to be associated with the UserProfile. + + Raises: + ValueError: If any of the identities are invalid or cannot be processed. + """ + identity_instances = [] + + for identity_name in identity_list: + # Validate or process identity_name here (e.g., check if it's a non-empty string) + if not identity_name or not isinstance(identity_name, str): + raise ValueError(f"Invalid name: {identity_name}") + + # Try to get the name by name, or create it if it doesn't exist + identity, created = model.objects.get_or_create(name=identity_name.strip()) + + # Optionally, handle the case where the name creation failed (if get_or_create does not meet your needs) + if not identity: + raise ValueError( + f"Failed to create or retrieve name with name: {identity_name}" + ) + + identity_instances.append(identity) + + return identity_instances + + +def extract_talent_data(data, files): + talent_data = { + "tech_journey": data.get("years_of_experience"), + "is_talent_status": data.get("talent_status") == "Yes", + "company_types": [get_id_or_create(CompanyTypes, ct) for ct in data.getlist("company_types", [])], + "department": [get_id_or_create(Department, dept) for dept in data.getlist("job_department", [])], + "role": [get_id_or_create(Roles, role) for role in data.getlist("job_roles", [])], + "skills": [get_id_or_create(Skill, skill) for skill in data.getlist("job_skills", [])], + "min_compensation": get_id_or_create(SalaryRange, data.get("min_compensation")), + "max_compensation": get_id_or_create(SalaryRange, data.get("max_compensation")), + "resume": files.get("resume"), + } + return talent_data + + +def extract_profile_id_data(data, files): + profile_data = { + "linkedin": data.get("linkedin"), + "instagram": data.get("instagram"), + "github": data.get("github"), + "twitter": data.get("twitter"), + "youtube": data.get("youtube"), + "personal": data.get("personal"), + "identity_sexuality": [get_id_or_create(SexualIdentities, si) for si in data.getlist("identity_sexuality", [])], + "identity_gender": [get_id_or_create(GenderIdentities, gi) for gi in data.getlist("gender_identities", [])], + "identity_ethic": [get_id_or_create(EthicIdentities, ei) for ei in data.getlist("identity_ethic", [])], + "identity_pronouns": [get_id_or_create(PronounsIdentities, pi) for pi in + data.getlist("pronouns_identities", [])], + "disability": data.get("disability") == "true", + "care_giver": data.get("care_giver") == "true", + "veteran_status": data.get("veteran_status"), + "how_connection_made": data.get("how_connection_made").lower(), + "postal_code": data.get("postal_code"), + "location": data.get("location"), + "state": data.get("state"), + "city": data.get("city"), + "photo": files.get("photo"), + } + return profile_data + + +def extract_user_data(data): + """ + Extracts and processes user-related data from the request data. + + Args: + data (dict): The request data containing user-related fields. + + Returns: + dict: A dictionary containing processed user data ready for creating or updating a user instance. + + The function processes the following user-related fields: + - is_mentee: Determines if the user is a mentee. + - is_mentor: Determines if the user is a mentor. + - Other fields can be added as per the application's requirements. + """ + + return { + "is_mentee": bool(data.get("is_mentee", "")), + "is_mentor": bool(data.get("is_mentor", "")), + } + + +def extract_company_data(data): + """ + Extracts and processes company-related data from the request data. + + Args: + data (dict): The request data containing user-related fields. + + Returns: + dict: A dictionary containing processed user data ready for creating or updating a user instance. + + The function processes the following user-related fields: + - company_name: The name of the user's company. + - company_url: The URL of the user's company. + - Other fields can be added as per the application's requirements. + """ + + return { + "company_name": data.get("company_name", ""), + "company_url": data.get("company_url", ""), + "company_id": data.get("company_id", ""), + } + + +def extract_profile_data(data, files): + """ + Extract and process profile-related data from the request. + + This function processes the incoming data and files related to the user's profile. It handles: + - Extracting profile data fields from the request. + - Processing URLs to ensure they are correctly formatted. + - Splitting comma-separated strings into lists. + - Handling file uploads for photos. + - Converting string 'True'/'False' or presence of value to boolean. + + :param data: The request data from which to extract profile information. + :param files: The request files which may contain the photo. + :return: A dictionary containing processed profile-related data. + """ + profile_data = { + "linkedin": prepend_https_if_not_empty(data.get("linkedin", "")), + "instagram": data.get("instagram", ""), + "github": prepend_https_if_not_empty(data.get("github", "")), + "twitter": data.get("twitter", ""), + "youtube": prepend_https_if_not_empty(data.get("youtube", "")), + "personal": prepend_https_if_not_empty(data.get("personal", "")), + "identity_sexuality": data.get("identity_sexuality", "").split(","), + "is_identity_sexuality_displayed": bool( + data.get("is_identity_sexuality_displayed", "") + ), + "identity_gender": data.get("gender_identities", "").split(","), + "is_identity_gender_displayed": bool( + data.get("is_identity_gender_displayed", "") + ), + "identity_ethic": data.get("identity_ethic", "").split(","), + "is_identity_ethic_displayed": bool( + data.get("is_identity_ethic_displayed", "") + ), + "identity_pronouns": data.get("pronouns_identities", "").split(",") + if data.get("pronouns_identities") + else "", + "disability": bool(data.get("disability", "")), + "is_disability_displayed": bool(data.get("is_disability_displayed", "")), + "care_giver": bool(data.get("care_giver", "")), + "is_care_giver_displayed": bool(data.get("is_care_giver_displayed", "")), + "veteran_status": data.get("veteran_status", ""), + "is_veteran_status_displayed": bool( + data.get("is_veteran_status_displayed", "") + ), + "how_connection_made": data.get("how_connection_made", "").lower(), + "is_pronouns_displayed": bool(data.get("is_pronouns_displayed", "")), + "marketing_monthly_newsletter": bool( + data.get("marketing_monthly_newsletter", "") + ), + "marketing_events": bool(data.get("marketing_events", "")), + "marketing_identity_based_programing": bool( + data.get("marketing_identity_based_programing", "") + ), + "marketing_jobs": bool(data.get("marketing_jobs", "")), + "marketing_org_updates": bool(data.get("marketing_org_updates", "")), + "postal_code": data.get("postal_code", ""), + "tbc_program_interest": data.get("tbc_program_interest", "").split(",") + if data.get("tbc_program_interest", "") + else None, + "photo": files["photo"] if "photo" in files else None, + } + + return profile_data + + +def extract_profile_data_id(data, files): + """ + Extract and process profile-related IDs data from the request. + + This function processes the incoming data and files related to the user's profile. It handles: + - Extracting profile data fields from the request. + - Processing URLs to ensure they are correctly formatted. + - Splitting comma-separated strings into lists. + - Handling file uploads for photos. + - Converting string 'True'/'False' or presence of value to boolean. + + :param data: The request data from which to extract profile information. + :param files: The request files which may contain the photo. + :return: A dictionary containing processed profile-related data. + """ + profile_data = { + "linkedin": prepend_https_if_not_empty(data.get("linkedin", "")), + "instagram": data.get("instagram", ""), + "github": prepend_https_if_not_empty(data.get("github", "")), + "twitter": data.get("twitter", ""), + "youtube": prepend_https_if_not_empty(data.get("youtube", "")), + "personal": prepend_https_if_not_empty(data.get("personal", "")), + "identity_sexuality": [get_id_or_create(SexualIdentities, si) for si in data.getlist("identity_sexuality", [])], + "identity_gender": [get_id_or_create(GenderIdentities, gi) for gi in data.getlist("gender_identities", [])], + "identity_ethic": [get_id_or_create(EthicIdentities, ei) for ei in data.getlist("identity_ethic", [])], + "identity_pronouns": [get_id_or_create(PronounsIdentities, pi) for pi in + data.getlist("pronouns_identities", [])], + "disability": bool(data.get("disability", "")), + "is_disability_displayed": bool(data.get("is_disability_displayed", "")), + "care_giver": bool(data.get("care_giver", "")), + "is_care_giver_displayed": bool(data.get("is_care_giver_displayed", "")), + "veteran_status": data.get("veteran_status", ""), + "is_veteran_status_displayed": bool( + data.get("is_veteran_status_displayed", "") + ), + "how_connection_made": data.get("how_connection_made", "").lower(), + "is_pronouns_displayed": bool(data.get("is_pronouns_displayed", "")), + "marketing_monthly_newsletter": bool( + data.get("marketing_monthly_newsletter", "") + ), + "marketing_events": bool(data.get("marketing_events", "")), + "marketing_identity_based_programing": bool( + data.get("marketing_identity_based_programing", "") + ), + "marketing_jobs": bool(data.get("marketing_jobs", "")), + "marketing_org_updates": bool(data.get("marketing_org_updates", "")), + "postal_code": data.get("postal_code", ""), + "tbc_program_interest": data.get("tbc_program_interest", "").split(",") + if data.get("tbc_program_interest", "") + else None, + "photo": files["photo"] if "photo" in files else None, + } + + return profile_data + + +def extract_talent_data(data, files): + """ + Extracts and processes talent-related data from the request. + + The function processes incoming data to structure it according to the MemberProfile model's needs. + It handles extracting and converting data, ensuring that multi-value fields are appropriately split and + that file fields are handled correctly. + + Args: + data (dict): The request data containing talent-related information. + files (dict): The uploaded files in the request. + + Returns: + dict: A dictionary containing processed talent data ready to be used in a MemberProfile serializer or model. + """ + + talent_data = { + "tech_journey": data.get("years_of_experience", []), + "is_talent_status": data.get("talent_status", False), + "company_types": data.get("company_types", "").split(",") + if data.get("company_types") + else [], + "department": data.get("job_department", "").split(",") + if data.get("job_department") + else [], + "role": data.get("job_roles", "").split(",") if data.get("job_roles") else [], + "skills": data.get("job_skills", "").split(",") + if data.get("job_skills") + else [], + "max_compensation": data.get("max_compensation", []), + "min_compensation": data.get("min_compensation", []), + "resume": files.get("resume") if "resume" in files else None, + } + + # Ensuring that the list fields containing IDs are actually lists of integers + for field in ["max_compensation", "min_compensation"]: + if isinstance(talent_data[field], list): + talent_data[field] = [int(i) for i in talent_data[field] if i.isdigit()] + + # Convert boolean fields from string to actual boolean values + talent_data["is_talent_status"] = bool(talent_data["is_talent_status"]) + + # Clean up list fields to ensure there are no empty strings + for list_field in ["company_types", "department", "role", "skills"]: + talent_data[list_field] = [ + item for item in talent_data[list_field] if item.strip() + ] + + return talent_data + + +def process_company_types(company_types): + """ + Process the given list of company types. It ensures that each company type is valid + and corresponds to a CompanyType instance in the database. If a company type does not + exist, it will be created. + + Args: + company_types (list): A list of company type names or IDs. + + Returns: + QuerySet: A QuerySet of CompanyType instances that are associated with the provided company types. + + Raises: + ValueError: If a company type is invalid or cannot be processed. + """ + company_type_instances = [] + for company_type in company_types: + # Skip empty strings or None values + if not company_type: + continue + + try: + # Attempt to get the CompanyType by name or ID + if isinstance(company_type, int): + # If company_type is an int, we assume it's an ID + company_type_instance, created = CompanyTypes.objects.get_or_create( + id=company_type + ) + else: + # If company_type is a string, we assume it's the name of the company type + company_type_instance, created = CompanyTypes.objects.get_or_create( + name=company_type + ) + + company_type_instances.append(company_type_instance) + except CompanyTypes.MultipleObjectsReturned: + # This block handles the case where get_or_create returns multiple objects + raise ValueError(f"Multiple company types found for: {company_type}") + except Exception as e: + # Handle other exceptions such as database errors + raise ValueError(f"Error processing company type {company_type}: {str(e)}") + + return company_type_instances + + +def process_roles(role_identifiers): + """ + Process and validate role identifiers before setting them in the MemberProfile. + + This function takes a list of role identifiers, which can be names or IDs, and returns the corresponding + Role instances after validating their existence in the database. If a role does not exist, it's created. + + Args: + role_identifiers (list): A list of role names or IDs. + + Returns: + QuerySet or list: A QuerySet or list of Role model instances to be associated with the MemberProfile. + + Raises: + ValueError: If any of the identifiers is invalid or if the role cannot be found or created. + """ + if not isinstance(role_identifiers, list) or not all(role_identifiers): + raise ValueError("Role identifiers must be a non-empty list.") + + roles_to_set = [] + for identifier in role_identifiers: + # Check and remove 'Add "' prefix and trailing '"' if present + if identifier.startswith('Add "') and identifier.endswith('"'): + identifier = identifier[5:-1].strip() + + try: + # If identifier is a role ID + if isinstance(identifier, int): + role = Roles.objects.get(id=identifier) + # If identifier is a role name + elif isinstance(identifier, str): + role, created = Roles.objects.get_or_create(name=identifier) + + if created: + print(f"Created new role: {identifier.name}") + else: + raise ValueError(f"Invalid role identifier: {identifier}") + + roles_to_set.append(role) + except Roles.DoesNotExist: + raise ValueError(f"Role not found for identifier: {identifier}") + except Roles.MultipleObjectsReturned: + raise ValueError(f"Multiple roles returned for identifier: {identifier}") + except Exception as e: + # Log the exception for debugging + print(f"Error in process_roles: {str(e)}") + # Re-raise the exception to be handled by the caller + raise + + return roles_to_set + + +def process_departments(department_names): + """ + Process and validate department names before setting them in the MemberProfile. + + This function ensures that all department names provided are valid and correspond to existing + Department instances in the database. If a department does not exist, it's created. + + Args: + department_names (list of str): A list of department names. + + Returns: + QuerySet: A QuerySet of Department instances to be associated with the MemberProfile. + + Raises: + ValueError: If any of the department names are invalid (e.g., empty strings or not matching any predefined departments). + """ + if not department_names or not isinstance(department_names, list): + raise ValueError("Department names should be a non-empty list.") + + department_set = [] + for name in department_names: + if not name: + # Handle empty string or None + raise ValueError("Department name cannot be empty or None.") + + # Check and remove 'Add "' prefix and trailing '"' if present + if name.startswith('Add "') and name.endswith('"'): + name = name[5:-1].strip() + + try: + # Check if department exists, if not, create it + k = name.strip() + department, created = Department.objects.get_or_create(name=k) + department_set.append(department) + if created: + print(f"Created new department: {department.name}") + except Exception as e: + # Log the exception and skip this department + print(f"Failed to create or retrieve department '{name.strip()}': {e}") + continue + + # Return a QuerySet or a list of Department instances + return department_set + + +def process_skills(skill_list): + """ + Process and validate skills before setting them in the MemberProfile. + + This function takes a list of skill names or identifiers, ensures that these skills + are present in the database (creating them if necessary), and returns a queryset + or list of Skill model instances. + + Args: + skill_list (list): A list of skill names or identifiers. + + Returns: + QuerySet: A QuerySet of Skill instances to be associated with the MemberProfile. + + Raises: + ValueError: If any of the skills are invalid or cannot be processed. + """ + skill_instances = [] + + for skill_name in skill_list: + # Validate or process skill_name here (e.g., check if it's a non-empty string) + if not skill_name or not isinstance(skill_name, str): + raise ValueError(f"Invalid skill name: {skill_name}") + + # Check and remove 'Add "' prefix and trailing '"' if present + if skill_name.startswith('Add "') and skill_name.endswith('"'): + skill_name = skill_name[5:-1].strip() + + # Try to get the skill by name, or create it if it doesn't exist + try: + skill, created = Skill.objects.get_or_create(name=skill_name) + if created: + print(f"Created new department: {skill.name}") + skill_instances.append(skill) + # Optionally, handle the case where the skill creation failed (if get_or_create does not meet your needs) + except Exception as e: + raise ValueError( + f"Failed to create or retrieve skill with name: {skill_name}. Error: {e}" + ) + return skill_instances + + +def process_compensation(compensation_data, default_value=None): + """ + Process and validate compensation data before setting it in the MemberProfile. + + Args: + compensation_data (list): A list containing compensation range IDs or values. + default_value (SalaryRange or None): The default SalaryRange to return if compensation_data is empty. + + Returns: + SalaryRange or None: The SalaryRange model instance to be associated with the MemberProfile, or the default value. + + Raises: + ValidationError: If the compensation data is not valid or does not meet the business requirements. + """ + if not compensation_data: + # If compensation_data is empty or None, return the default_value + return default_value + + compensation_to_set = [] + for comp_id in compensation_data: + try: + # Assuming comp_id is the ID of the SalaryRange, try to fetch the SalaryRange instance + salary_range = SalaryRange.objects.get(id=comp_id) + compensation_to_set.append(salary_range) + except SalaryRange.DoesNotExist: + # Handle the case where the SalaryRange does not exist for the given id + raise ValidationError(f"Salary range with id {comp_id} does not exist.") + except SalaryRange.MultipleObjectsReturned: + # Handle the case where multiple SalaryRanges are returned for the given id + raise ValidationError(f"Multiple salary ranges found for id {comp_id}.") + except ValueError: + # Handle the case where comp_id is not a valid integer (if ids are integers) + raise ValidationError(f"Invalid id: {comp_id}. Id must be an integer.") + + return compensation_to_set[0] if compensation_to_set else default_value + + +def get_current_company_data(user): + """ + Retrieve data of the company where the given user is currently employed. + + This function fetches the company associated with the given user as a current employee. It extracts + and returns relevant company data, including the company's ID, name, logo, size, and industries. + + Args: + user (CustomUser): The user whose current company data is to be retrieved. + + Returns: + dict: A dictionary containing the company's ID, name, logo URL, size, and list of industries, + if the company is found. For example: + { + "id": 1, + "company_name": "Example Corp", + "logo": "/media/logo_pics/default-logo.jpeg", + "company_size": "501-1000", + "industries": ["Tech", "Media"] + } + None: If the user is not currently associated with any company or the company does not exist. + + Raises: + CompanyProfile.DoesNotExist: If no CompanyProfile is associated with the user as a current employee. + """ + try: + company = CompanyProfile.objects.get(current_employees=user) + return { + "id": company.id, + "company_name": company.company_name, + "logo": company.logo.url, + "company_size": company.company_size, + "industries": [industry.name for industry in company.industries.all()], + } + except CompanyProfile.DoesNotExist: + return None diff --git a/utils/emails.py b/utils/emails.py index 2e40c51..ec312fd 100644 --- a/utils/emails.py +++ b/utils/emails.py @@ -1,6 +1,12 @@ import os + +from django.core.mail import EmailMessage +from django.template.loader import render_to_string from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail +from utils.logging_helper import get_logger + +logger = get_logger(__name__) def send_dynamic_email(email_data): @@ -34,3 +40,28 @@ def send_dynamic_email(email_data): except Exception as e: print(f"An error occurred: {e}") return None + + +def send_password_email(email, first_name, user, reset_link): + """ + Send a password reset email to the user. + """ + mail_subject = 'Password Reset Request' + + context = { + 'username': first_name, + 'reset_link': reset_link, + } + + message = render_to_string('emails/password_reset_email.txt', context=context) + email_msg = EmailMessage(mail_subject, message, 'notifications@app.techbychoice.org', [email]) + email_msg.extra_headers = { + 'email_template': 'emails/password_reset_email.html', + 'username': first_name, + 'reset_link': reset_link, + } + + try: + email_msg.send() + except Exception as e: + logger.error(f"Error while sending password reset email: {str(e)}") diff --git a/utils/helper.py b/utils/helper.py index 1c14684..0fe9b64 100644 --- a/utils/helper.py +++ b/utils/helper.py @@ -13,10 +13,6 @@ def get_quill(value): return quill -def prepend_https_if_not_empty(url): - return "https://" + url if url else url - - def generate_random_password(): characters = string.ascii_letters + string.digits + string.punctuation password = "".join(random.choice(characters) for i in range(8)) diff --git a/utils/profile_utils.py b/utils/profile_utils.py index fad9ad9..4f8f13d 100644 --- a/utils/profile_utils.py +++ b/utils/profile_utils.py @@ -217,4 +217,65 @@ def calculate_profile_completeness(profile): """ total_fields = len(profile._meta.fields) filled_fields = sum(1 for f in profile._meta.fields if getattr(profile, f.name) not in [None, '']) - return (filled_fields / total_fields) * 100 \ No newline at end of file + return (filled_fields / total_fields) * 100 + + +def update_talent_profile(user, talent_data): + """ + Create or update the MemberProfile for a given user. + + Args: + user (User model instance): The user for whom the MemberProfile needs to be created or updated. + talent_data (dict): A dictionary containing all the necessary data to create or update a MemberProfile. + + Returns: + MemberProfile: The created or updated MemberProfile instance. + + Raises: + ValidationError: If the provided data is not valid to create or update a MemberProfile. + """ + try: + # Check if the user already has a MemberProfile + talent_profile = MemberProfile.objects.get(user=user) + + # Update or set fields in talent_profile from talent_data + talent_profile.tech_journey = talent_data.get( + "tech_journey", talent_profile.tech_journey + ) + talent_profile.is_talent_status = talent_data.get( + "talent_status", talent_profile.is_talent_status + ) + + # Handle many-to-many fields like company_types, roles, departments, skills, etc. + # TODO: Testing Fix | we can use the process_identity_field() to simplify the code here + talent_profile.company_types.set( + process_company_types(talent_data.get("company_types", [])) + ) + talent_profile.role.set(process_roles(talent_data.get("role", []))) + # Department is one that's messed up on prod so it's one I'm checking first + talent_profile.department.set( + process_departments(talent_data.get("department", [])) + ) + talent_profile.skills.set(process_skills(talent_data.get("skills", []))) + + # Handle compensation ranges + talent_profile.min_compensation = process_compensation( + talent_data.get("min_compensation", []) + ) + talent_profile.max_compensation = process_compensation( + talent_data.get("max_compensation", []) + ) + + # Handle file fields like resume + if "resume" in talent_data and talent_data["resume"] is not None: + talent_profile.resume = talent_data["resume"] + + # Save the updated profile + talent_profile.save() + return talent_profile + + except Exception as e: + # Log the exception for debugging + print(f"Error in update_talent_profile: {str(e)}") + # Re-raise the exception to be handled by the caller + raise \ No newline at end of file diff --git a/utils/urls_utils.py b/utils/urls_utils.py index 387f8c1..2eabf61 100644 --- a/utils/urls_utils.py +++ b/utils/urls_utils.py @@ -4,7 +4,7 @@ from urllib.parse import urlparse, urlunparse # Import the custom logging utilities -from .logging_utils import get_logger, log_exception, timed_function, sanitize_log_data +from utils.logging_helper import get_logger, log_exception, timed_function, sanitize_log_data # Get a logger for this module logger = get_logger(__name__) @@ -37,16 +37,8 @@ def prepend_https_if_not_empty(url): >>> prepend_https_if_not_empty(None) '' """ - logger.info(f"Processing URL: {sanitize_log_data({'url': url})}") - - if not url: - logger.debug("Empty URL provided, returning empty string") - return '' - - if not url.startswith(('http://', 'https://')): - url = 'https://' + url - logger.debug(f"Prepended 'https://' to URL: {sanitize_log_data({'url': url})}") - + if url and not url.startswith(("http://", "https://")): + return f"https://{url}" return url diff --git a/apps/core/util.py b/utils/util.py similarity index 99% rename from apps/core/util.py rename to utils/util.py index b074b87..1b1b8b6 100644 --- a/apps/core/util.py +++ b/utils/util.py @@ -3,15 +3,15 @@ from rest_framework.exceptions import ValidationError from utils.errors import CustomException -from .models import ( +from apps.core.models import ( UserProfile, SexualIdentities, GenderIdentities, EthicIdentities, PronounsIdentities, ) -from .serializers import UpdateCustomUserSerializer, UserProfileSerializer -from ..company.models import ( +from apps.core.serializers import UpdateCustomUserSerializer, UserProfileSerializer +from apps.company.models import ( CompanyTypes, Department, Skill, @@ -19,7 +19,7 @@ Roles, CompanyProfile, ) -from ..member.models import MemberProfile +from apps.member.models import MemberProfile User = get_user_model()