From 701fb4e23c7af15433c1218dba9af98ad2cff6f1 Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Fri, 3 Feb 2023 13:28:22 -0800 Subject: [PATCH 01/72] view survey responses from Project detail view --- .../project/templates/project/project_detail.html | 10 ++++++++++ coldfront/core/project/views.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/coldfront/core/project/templates/project/project_detail.html b/coldfront/core/project/templates/project/project_detail.html index ac50576e4..f36a836b8 100644 --- a/coldfront/core/project/templates/project/project_detail.html +++ b/coldfront/core/project/templates/project/project_detail.html @@ -138,6 +138,15 @@

{% endif %}

+ + + {% if survey_answers %} +

+ Survey Responses: + +

+ {% endif %} + @@ -466,4 +475,5 @@

+{% include 'project/project_request/savio/project_request_survey_modal.html' with survey_form=survey_answers %} {% endblock %} diff --git a/coldfront/core/project/views.py b/coldfront/core/project/views.py index 6f1cc08e5..3c3037231 100644 --- a/coldfront/core/project/views.py +++ b/coldfront/core/project/views.py @@ -34,12 +34,14 @@ ProjectSearchForm, ProjectUpdateForm, ProjectUserUpdateForm) +from coldfront.core.project.forms_.new_project_forms.request_forms import SavioProjectSurveyForm from coldfront.core.project.models import (Project, ProjectReview, ProjectReviewStatusChoice, ProjectStatusChoice, ProjectUser, ProjectUserRoleChoice, ProjectUserStatusChoice, - ProjectUserRemovalRequest) + ProjectUserRemovalRequest, + SavioProjectAllocationRequest) from coldfront.core.project.utils import (annotate_queryset_with_cluster_name, is_primary_cluster_project) from coldfront.core.project.utils_.addition_utils import can_project_purchase_service_units @@ -269,6 +271,13 @@ def get_context_data(self, **kwargs): context['can_request_sec_dir'] = \ pi_eligible_to_request_secure_dir(self.request.user) + # show survey responses if available + allocation_request = SavioProjectAllocationRequest.objects.filter( + project=self.object).first() + if allocation_request: + context['survey_answers'] = SavioProjectSurveyForm( + initial=allocation_request.survey_answers, disable_fields=True) + context['user_agreement_signed'] = \ access_agreement_signed(self.request.user) From 8c4f709670fb46a991b13c42adf1e86baf2ae981 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 8 Feb 2023 11:41:23 -0800 Subject: [PATCH 02/72] Add first pass at hiding multiple email address-related functionality in the User Profile --- coldfront/core/user/views.py | 105 +++++++++++++++++++++++------------ 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/coldfront/core/user/views.py b/coldfront/core/user/views.py index cdd7cad9a..d7737cb1e 100644 --- a/coldfront/core/user/views.py +++ b/coldfront/core/user/views.py @@ -60,6 +60,10 @@ class UserProfile(TemplateView): template_name = 'user/user_profile.html' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._get_flag_states() + def dispatch(self, request, *args, viewed_username=None, **kwargs): # viewing another user profile requires permissions if viewed_username: @@ -96,59 +100,92 @@ def get_context_data(self, viewed_username=None, **kwargs): requester_is_viewed_user = viewed_user == self.request.user if requester_is_viewed_user: - self.update_context_with_identity_linking_request_data(context) + self._update_context_with_identity_linking_request_data(context) context['help_email'] = import_from_settings('CENTER_HELP_EMAIL') - # Only display the "Other Email Addresses" section for - # coldfront.core.user.models.EmailAddress if basic auth. is enabled. - is_basic_auth_enabled = flag_enabled('BASIC_AUTH_ENABLED') context['requester_is_viewed_user'] = requester_is_viewed_user - context['primary_address_updatable'] = ( - is_basic_auth_enabled and requester_is_viewed_user) + + self._update_context_with_email_and_account_data(context, viewed_user) + + if self._flag_lrc_only: + self._update_context_with_billing_data(context, viewed_user) + + context['is_lbl_employee'] = is_lbl_employee(viewed_user) + + return context + + def _get_flag_states(self): + """Store the states of various flags needed by the class.""" + self._flag_basic_auth_enabled = flag_enabled('BASIC_AUTH_ENABLED') + self._flag_lrc_only = flag_enabled('LRC_ONLY') + self._flag_multiple_email_addresses_allowed = flag_enabled( + 'MULTIPLE_EMAIL_ADDRESSES_ALLOWED') + self._flag_sso_enabled = flag_enabled('SSO_ENABLED') + + @staticmethod + def _update_context_with_billing_data(context, viewed_user): + """Update the given context dictionary with fields relating to + billing IDs. Take the currently-viewed User object as an input + to make determinations.""" + billing_id = 'N/A' + try: + user_profile = viewed_user.userprofile + except UserProfileModel.DoesNotExist: + message = ( + f'User {viewed_user.username} unexpectedly has no ' + f'UserProfile.') + logger.error(message) + else: + billing_activity = user_profile.billing_activity + if billing_activity: + billing_id = billing_activity.full_id() + context['monthly_user_account_fee_billing_id'] = billing_id + + def _update_context_with_email_and_account_data(self, context, + viewed_user): + """Update the given context directory with fields relating to + user emails, passwords, and third-party accounts. Take the + currently-viewed User object as an input to make + determinations.""" + requester_is_viewed_user = viewed_user == self.request.user + context['change_password_enabled'] = ( - is_basic_auth_enabled and requester_is_viewed_user) - context['core_user_email_addresses_visible'] = is_basic_auth_enabled + self._flag_basic_auth_enabled and requester_is_viewed_user) + + context['primary_address_updatable'] = ( + self._flag_basic_auth_enabled and + self._flag_multiple_email_addresses_allowed and + requester_is_viewed_user) + + # Use coldfront.core.user.models.EmailAddress if basic auth. is + # enabled. + context['core_user_email_addresses_visible'] = ( + self._flag_basic_auth_enabled and + self._flag_multiple_email_addresses_allowed) if context['core_user_email_addresses_visible']: context['other_emails'] = EmailAddress.objects.filter( user=viewed_user, is_primary=False).order_by('email') context['core_user_email_addresses_updatable'] = \ requester_is_viewed_user - # Only display the "Other Email Addresses" section for - # allauth.account.models.EmailAddress if SSO is enabled. - is_sso_enabled = flag_enabled('SSO_ENABLED') - context['allauth_email_addresses_visible'] = is_sso_enabled + # Use allauth.account.models.EmailAddress if SSO is enabled. + context['allauth_email_addresses_visible'] = ( + self._flag_sso_enabled and + self._flag_multiple_email_addresses_allowed) if context['allauth_email_addresses_visible']: context['allauth_email_addresses_updatable'] = \ requester_is_viewed_user - # Only display the "Third-Party Accounts" section if SSO is enabled. - context['third_party_accounts_visible'] = is_sso_enabled + # Display the "Third-Party Accounts" section if SSO is enabled. + context['third_party_accounts_visible'] = ( + self._flag_sso_enabled and + self._flag_multiple_email_addresses_allowed) if context['third_party_accounts_visible']: context['third_party_accounts_updatable'] = \ requester_is_viewed_user - if flag_enabled('LRC_ONLY'): - billing_id = 'N/A' - try: - user_profile = viewed_user.userprofile - except UserProfileModel.DoesNotExist: - message = ( - f'User {viewed_user.username} unexpectedly has no ' - f'UserProfile.') - logger.error(message) - else: - billing_activity = user_profile.billing_activity - if billing_activity: - billing_id = billing_activity.full_id() - context['monthly_user_account_fee_billing_id'] = billing_id - - context['is_lbl_employee'] = is_lbl_employee(viewed_user) - - return context - - def update_context_with_identity_linking_request_data(self, context): + def _update_context_with_identity_linking_request_data(self, context): """Update the given context dictionary with fields relating to IdentityLinkingRequests. From fed0684249e8a7eaff741d614d6a2603e3633a03 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Feb 2023 10:35:15 -0800 Subject: [PATCH 03/72] Remove admin view for old EmailAddress model; integrate its functionality into admin view for new one --- coldfront/core/account/admin.py | 83 +++++++++++++++++++++++++++++++++ coldfront/core/user/admin.py | 81 -------------------------------- 2 files changed, 83 insertions(+), 81 deletions(-) create mode 100644 coldfront/core/account/admin.py diff --git a/coldfront/core/account/admin.py b/coldfront/core/account/admin.py new file mode 100644 index 000000000..c68a52a75 --- /dev/null +++ b/coldfront/core/account/admin.py @@ -0,0 +1,83 @@ +from django.contrib import admin +from django.contrib import messages +from django.core.exceptions import ValidationError + +from allauth.account.models import EmailAddress + +from coldfront.core.account.utils.queries import update_user_primary_email_address + +import logging + + +logger = logging.getLogger(__name__) + + +admin.site.unregister(EmailAddress) + + +@admin.register(EmailAddress) +class EmailAddressAdmin(admin.ModelAdmin): + + actions = ('make_primary', 'make_verified', ) + readonly_fields = ('primary', 'verified', ) + + def delete_model(self, request, obj): + if obj.primary: + raise ValidationError( + 'Cannot delete primary email. Unset primary status in list ' + 'display before deleting.') + else: + super().delete_model(request, obj) + + @admin.action(description='Make selected primary') + def make_primary(self, request, queryset): + """Set the EmailAddresses in the given queryset as the primary + addresses of their respective users. + + Currently, admins are limited to setting at most one address at + a time.""" + if queryset.count() > 1: + raise ValidationError( + 'Cannot set more than one primary email address at a time.') + for email_address in queryset: + user = email_address.user + try: + update_user_primary_email_address(email_address) + except ValueError: + raise ValidationError( + 'Cannot set an unverified email address as primary.') + except Exception as e: + message = ( + f'Encountered unexpected exception when updating User ' + f'{user.pk}\'s primary EmailAddress to ' + f'{email_address.pk}. Details:') + logger.error(message) + logger.exception(e) + raise ValidationError( + f'Failed to set {email_address.pk} as primary. See the ' + f'log for details.') + else: + message = ( + f'Set User {user.pk}\'s primary EmailAddress to ' + f'{email_address.email}.') + messages.success(request, message) + + def delete_queryset(self, request, queryset): + """Delete EmailAddresses in the given queryset, skipping those + that are primary.""" + num_primary, num_non_primary = 0, 0 + for email_address in queryset: + if email_address.primary: + num_primary = num_primary + 1 + else: + email_address.delete() + num_non_primary = num_non_primary + 1 + + success_message = ( + f'Deleted {num_non_primary} non-primary EmailAddresses.') + messages.success(request, success_message) + + if num_primary > 0: + error_message = ( + f'Skipped deleting {num_primary} primary EmailAddresses.') + messages.error(request, error_message) \ No newline at end of file diff --git a/coldfront/core/user/admin.py b/coldfront/core/user/admin.py index be0ae38a6..7f4a7c342 100644 --- a/coldfront/core/user/admin.py +++ b/coldfront/core/user/admin.py @@ -1,14 +1,6 @@ from django.contrib import admin -from django.contrib import messages -from django.core.exceptions import ValidationError from coldfront.core.user.models import UserProfile, EmailAddress -from coldfront.core.user.utils import update_user_primary_email_address - -import logging - - -logger = logging.getLogger(__name__) @admin.register(UserProfile) @@ -28,79 +20,6 @@ def last_name(self, obj): return obj.user.last_name -@admin.register(EmailAddress) -class EmailAddressAdmin(admin.ModelAdmin): - list_display = ('user', 'email', 'is_primary', 'is_verified', ) - ordering = ('user', '-is_primary', '-is_verified', 'email', ) - list_filter = ('is_primary', 'is_verified', ) - search_fields = [ - 'user__username', 'user__first_name', 'user__last_name', 'email'] - fields = ('email', 'user', 'is_primary', 'is_verified', ) - actions = ('make_primary', ) - readonly_fields = ('is_primary', 'is_verified', ) - - def delete_model(self, request, obj): - if obj.is_primary: - raise ValidationError( - 'Cannot delete primary email. Unset primary status in list ' - 'display before deleting.') - else: - super().delete_model(request, obj) - - @admin.action(description='Make selected primary') - def make_primary(self, request, queryset): - """Set the EmailAddresses in the given queryset as the primary - addresses of their respective users. - - Currently, admins are limited to setting at most one address at - a time.""" - if queryset.count() > 1: - raise ValidationError( - 'Cannot set more than one primary email address at a time.') - for email_address in queryset: - user = email_address.user - try: - update_user_primary_email_address(email_address) - except ValueError: - raise ValidationError( - 'Cannot set an unverified email address as primary.') - except Exception as e: - message = ( - f'Encountered unexpected exception when updating User ' - f'{user.pk}\'s primary EmailAddress to ' - f'{email_address.pk}. Details:') - logger.error(message) - logger.exception(e) - raise ValidationError( - f'Failed to set {email_address.pk} as primary. See the ' - f'log for details.') - else: - message = ( - f'Set User {user.pk}\'s primary EmailAddress to ' - f'{email_address.email}.') - messages.success(request, message) - - def delete_queryset(self, request, queryset): - """Delete EmailAddresses in the given queryset, skipping those - that are primary.""" - num_primary, num_non_primary = 0, 0 - for email_address in queryset: - if email_address.is_primary: - num_primary = num_primary + 1 - else: - email_address.delete() - num_non_primary = num_non_primary + 1 - - success_message = ( - f'Deleted {num_non_primary} non-primary EmailAddresses.') - messages.success(request, success_message) - - if num_primary > 0: - error_message = ( - f'Skipped deleting {num_primary} primary EmailAddresses.') - messages.error(request, error_message) - - class EmailAddressInline(admin.TabularInline): model = EmailAddress extra = 0 From 3987136596bdc0f5b29c27979d3dbb7d64b88184 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Feb 2023 10:38:19 -0800 Subject: [PATCH 04/72] Remove redundant email views already provided by allauth; remove references to old EmailAddress in user views --- coldfront/core/user/forms.py | 29 --- .../user/user_add_email_address.html | 31 --- .../user/templates/user/user_profile.html | 65 ------ .../user_update_primary_email_address.html | 38 --- coldfront/core/user/urls.py | 18 -- coldfront/core/user/views.py | 221 +----------------- 6 files changed, 5 insertions(+), 397 deletions(-) delete mode 100644 coldfront/core/user/templates/user/user_add_email_address.html delete mode 100644 coldfront/core/user/templates/user/user_update_primary_email_address.html diff --git a/coldfront/core/user/forms.py b/coldfront/core/user/forms.py index ae742651d..ee29a014e 100644 --- a/coldfront/core/user/forms.py +++ b/coldfront/core/user/forms.py @@ -240,40 +240,11 @@ def set_acknowledgement_help_text(self): field.help_text = template.format('LBNL', 'LBNL IT Division') -class EmailAddressAddForm(forms.Form): - - email = forms.EmailField(max_length=100, required=True) - - def clean_email(self): - cleaned_data = super().clean() - email = cleaned_data['email'].lower() - if (User.objects.filter(email=email).exists() or - EmailAddress.objects.filter(email=email).exists()): - raise forms.ValidationError( - f'Email address {email} is already in use.') - return email - - class UserReactivateForm(forms.Form): email = forms.EmailField(max_length=100, required=True) -class PrimaryEmailAddressSelectionForm(forms.Form): - - email_address = forms.ModelChoiceField( - label='New Primary Email Address', - queryset=EmailAddress.objects.none(), - required=True, - widget=forms.RadioSelect()) - - def __init__(self, *args, **kwargs): - user = kwargs.pop('user') - super().__init__(*args, **kwargs) - self.fields['email_address'].queryset = EmailAddress.objects.filter( - user=user, is_verified=True, is_primary=False) - - class VerifiedEmailAddressPasswordResetForm(PasswordResetForm): """A subclass of django.contrib.auth.forms.PasswordResetForm that uses EmailAddress.""" diff --git a/coldfront/core/user/templates/user/user_add_email_address.html b/coldfront/core/user/templates/user/user_add_email_address.html deleted file mode 100644 index aae512d65..000000000 --- a/coldfront/core/user/templates/user/user_add_email_address.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "common/base.html" %} -{% load crispy_forms_tags %} -{% load static %} - - -{% block title %} -Add Email Address -{% endblock %} - - -{% block content %} - -

Add an Email Address

-
- -

Associate another email address with your account. Once verified, you may authenticate to this portal using this address.

- -
-
-
- {% csrf_token %} - {{ form|crispy }} - - - Cancel - -
-
-
- -{% endblock %} diff --git a/coldfront/core/user/templates/user/user_profile.html b/coldfront/core/user/templates/user/user_profile.html index 82090b203..6576a708b 100644 --- a/coldfront/core/user/templates/user/user_profile.html +++ b/coldfront/core/user/templates/user/user_profile.html @@ -185,71 +185,6 @@


- {% if core_user_email_addresses_visible %} -
-
- - Other Email Addresses{% if not requester_is_viewed_user %}: {{ viewed_user.username }}{% endif %} - {% if core_user_email_addresses_updatable %} - - {% endif %} -
-
-

Below are other email addresses associated with your account. You may authenticate to this portal using any verified addresses.

-

You can verify an address by clicking the “Verify” button, which will send a verification email to the address. The address will remain unverified until you have clicked the link provided in that email.

-
- - - - - - - - {% for email in other_emails %} - - - - - - {% endfor %} - -
Email AddressStatusActions
{{ email.email }} - {% if email.is_verified %} - Verified - {% else %} - Verification Pending - {% endif %} - - {% if core_user_email_addresses_updatable %} - {% if not email.is_verified %} -
- {% csrf_token %} - -
- {% endif %} -
- {% csrf_token %} - -
- {% else %} - N/A - {% endif %} -
-
-
-
-
- {% endif %} - {% if allauth_email_addresses_visible %}
diff --git a/coldfront/core/user/templates/user/user_update_primary_email_address.html b/coldfront/core/user/templates/user/user_update_primary_email_address.html deleted file mode 100644 index a1e9be1f6..000000000 --- a/coldfront/core/user/templates/user/user_update_primary_email_address.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "common/base.html" %} -{% load crispy_forms_tags %} -{% load static %} - - -{% block title %} -Update Primary Email Address -{% endblock %} - - -{% block content %} - -

Update Primary Email Address

-
- -

Select a verified email address below to set as your new primary address, which receives email notifications and may be used to reset your password.

-

Your current primary email address is: {{ request.user.email }}.

- -
-
- {% if has_verified_non_primary_emails %} -
- {% csrf_token %} - {{ form|crispy }} - - - Cancel - -
- {% else %} -
- You have no other verified email addresses. Please add one before proceeding. -
- {% endif %} -
-
- -{% endblock %} diff --git a/coldfront/core/user/urls.py b/coldfront/core/user/urls.py index c4b7c46a2..265c816a4 100644 --- a/coldfront/core/user/urls.py +++ b/coldfront/core/user/urls.py @@ -78,24 +78,6 @@ PasswordResetCompleteView.as_view( template_name='user/passwords/password_reset_complete.html'), name='password-reset-complete'), - - # Email views - f_path('add-email-address', - user_views.EmailAddressAddView.as_view(), - name='add-email-address'), - f_path('verify-email-address////', - user_views.verify_email_address, - name='verify-email-address'), - f_path('send-email-verification-email/', - user_views.SendEmailAddressVerificationEmailView.as_view(), - name='send-email-verification-email'), - f_path('remove-email-address/', - user_views.RemoveEmailAddressView.as_view(), - name='remove-email-address'), - f_path('update-primary-email-address', - user_views.UpdatePrimaryEmailAddressView.as_view(), - name='update-primary-email-address'), - ] diff --git a/coldfront/core/user/views.py b/coldfront/core/user/views.py index cdd7cad9a..4faff26e0 100644 --- a/coldfront/core/user/views.py +++ b/coldfront/core/user/views.py @@ -8,7 +8,6 @@ from django.contrib.auth.views import PasswordChangeView from django.core.exceptions import ImproperlyConfigured from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db import IntegrityError from django.db.models import BooleanField, Prefetch from django.db.models.expressions import ExpressionWrapper, Q from django.db.models.functions import Lower @@ -24,24 +23,21 @@ from django.views.generic import CreateView, ListView, TemplateView from django.views.generic.edit import FormView +from allauth.account.models import EmailAddress + +from coldfront.core.account.utils.queries import update_user_primary_email_address from coldfront.core.allocation.utils import has_cluster_access from coldfront.core.project.models import Project, ProjectUser from coldfront.core.user.models import IdentityLinkingRequest, IdentityLinkingRequestStatusChoice from coldfront.core.user.models import UserProfile as UserProfileModel -from coldfront.core.user.forms import EmailAddressAddForm from coldfront.core.user.forms import UserReactivateForm -from coldfront.core.user.forms import PrimaryEmailAddressSelectionForm from coldfront.core.user.forms import UserAccessAgreementForm from coldfront.core.user.forms import UserProfileUpdateForm from coldfront.core.user.forms import UserRegistrationForm from coldfront.core.user.forms import UserSearchForm, UserSearchListForm -from coldfront.core.user.models import EmailAddress from coldfront.core.user.utils import CombinedUserSearch -from coldfront.core.user.utils import ExpiringTokenGenerator from coldfront.core.user.utils import send_account_activation_email from coldfront.core.user.utils import send_account_already_active_email -from coldfront.core.user.utils import send_email_verification_email -from coldfront.core.user.utils import update_user_primary_email_address from coldfront.core.user.utils_.host_user_utils import is_lbl_employee from coldfront.core.utils.common import (import_from_settings, utc_now_offset_aware) @@ -100,21 +96,6 @@ def get_context_data(self, viewed_username=None, **kwargs): context['help_email'] = import_from_settings('CENTER_HELP_EMAIL') - # Only display the "Other Email Addresses" section for - # coldfront.core.user.models.EmailAddress if basic auth. is enabled. - is_basic_auth_enabled = flag_enabled('BASIC_AUTH_ENABLED') - context['requester_is_viewed_user'] = requester_is_viewed_user - context['primary_address_updatable'] = ( - is_basic_auth_enabled and requester_is_viewed_user) - context['change_password_enabled'] = ( - is_basic_auth_enabled and requester_is_viewed_user) - context['core_user_email_addresses_visible'] = is_basic_auth_enabled - if context['core_user_email_addresses_visible']: - context['other_emails'] = EmailAddress.objects.filter( - user=viewed_user, is_primary=False).order_by('email') - context['core_user_email_addresses_updatable'] = \ - requester_is_viewed_user - # Only display the "Other Email Addresses" section for # allauth.account.models.EmailAddress if SSO is enabled. is_sso_enabled = flag_enabled('SSO_ENABLED') @@ -426,7 +407,7 @@ def get_queryset(self): users = users.filter(username__icontains=data.get('username')) if data.get('email'): - _users = EmailAddress.objects.filter(is_primary=False, email__icontains=data.get('email'))\ + _users = EmailAddress.objects.filter(primary=False, email__icontains=data.get('email'))\ .order_by('user').values_list('user__id') users = users.filter(Q(email__icontains=data.get('email')) | Q(id__in=_users)) else: @@ -642,7 +623,7 @@ def activate_user_account(request, uidb64=None, token=None): logger.info( f'Created EmailAddress {email_address.pk} for User ' f'{user.pk} and email {email}.') - email_address.is_verified = True + email_address.verified = True email_address.save() update_user_primary_email_address(email_address) except Exception as e: @@ -708,198 +689,6 @@ def user_access_agreement(request): return render(request, template_name, context={'form': form}) -class EmailAddressAddView(LoginRequiredMixin, FormView): - form_class = EmailAddressAddForm - template_name = 'user/user_add_email_address.html' - - logger = logging.getLogger(__name__) - - def form_valid(self, form): - form_data = form.cleaned_data - email = form_data['email'] - try: - email_address = EmailAddress.objects.create( - user=self.request.user, email=email, is_verified=False, - is_primary=False) - except IntegrityError: - self.logger.error( - f'EmailAddress {email} unexpectedly already exists.') - message = ( - 'Unexpected server error. Please contact an administrator.') - messages.error(self.request, message) - else: - self.logger.info( - f'Created EmailAddress {email_address.pk} for User ' - f'{self.request.user.pk}.') - try: - send_email_verification_email(email_address) - except Exception as e: - message = 'Failed to send verification email. Details:' - logger.error(message) - logger.exception(e) - message = ( - f'Added {email_address.email} to your account, but failed ' - f'to send verification email. You may try to resend it ' - f'from the User Profile.') - messages.warning(self.request, message) - else: - message = ( - f'Added {email_address.email} to your account. Please ' - f'verify it by clicking the link sent to your email.') - messages.success(self.request, message) - return super().form_valid(form) - - def get_success_url(self): - return reverse('user-profile') - - -class SendEmailAddressVerificationEmailView(LoginRequiredMixin, View): - - def dispatch(self, request, *args, **kwargs): - pk = self.kwargs.get('pk') - self.email_address = get_object_or_404(EmailAddress, pk=pk) - if self.email_address.user != request.user: - message = ( - 'You may not send a verification email to an email address ' - 'not associated with your account.') - messages.error(request, message) - return HttpResponseRedirect(reverse('user-profile')) - if self.email_address.is_verified: - logger.error( - f'EmailAddress {self.email_address.pk} is unexpectedly ' - f'already verified.') - message = f'{self.email_address.email} is already verified.' - messages.warning(request, message) - return HttpResponseRedirect(reverse('user-profile')) - return super().dispatch(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - try: - send_email_verification_email(self.email_address) - except Exception as e: - message = 'Failed to send verification email. Details:' - logger.error(message) - logger.exception(e) - message = ( - f'Failed to send verification email to ' - f'{self.email_address.email}. Please contact an administrator ' - f'if the problem persists.') - messages.error(request, message) - else: - message = ( - f'Please click on the link sent to {self.email_address.email} ' - f'to verify it.') - messages.success(request, message) - return HttpResponseRedirect(reverse('user-profile')) - - -def verify_email_address(request, uidb64=None, eaidb64=None, token=None): - try: - user_pk = int(force_text(urlsafe_base64_decode(uidb64))) - email_pk = int(force_text(urlsafe_base64_decode(eaidb64))) - email_address = EmailAddress.objects.get(pk=email_pk) - user = User.objects.get(pk=user_pk) - if email_address.user != user: - user = None - except: - user = None - if user and token: - if ExpiringTokenGenerator().check_token(user, token): - email_address.is_verified = True - email_address.save() - logger.info(f'EmailAddress {email_address.pk} has been verified.') - message = f'{email_address.email} has been verified.' - messages.success(request, message) - else: - message = ( - 'Invalid verification token. Please try again, or contact an ' - 'administrator if the problem persists.') - messages.error(request, message) - else: - message = ( - f'Failed to activate account. Please contact an administrator.') - messages.error(request, message) - return redirect(reverse('user-profile')) - - -class RemoveEmailAddressView(LoginRequiredMixin, View): - - def dispatch(self, request, *args, **kwargs): - pk = self.kwargs.get('pk') - self.email_address = get_object_or_404(EmailAddress, pk=pk) - if self.email_address.user != request.user: - message = ( - 'You may not remove an email address not associated with your ' - 'account.') - messages.error(request, message) - return HttpResponseRedirect(reverse('user-profile')) - return super().dispatch(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - self.email_address.delete() - message = ( - f'{self.email_address.email} has been removed from your account.') - messages.success(request, message) - return HttpResponseRedirect(reverse('user-profile')) - - -class UpdatePrimaryEmailAddressView(LoginRequiredMixin, FormView): - - form_class = PrimaryEmailAddressSelectionForm - template_name = 'user/user_update_primary_email_address.html' - login_url = '/' - - error_message = 'Unexpected failure. Please contact an administrator.' - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['has_verified_non_primary_emails'] = \ - EmailAddress.objects.filter( - user=self.request.user, is_verified=True, is_primary=False) - return context - - def form_valid(self, form): - user = self.request.user - form_data = form.cleaned_data - new_primary = form_data['email_address'] - - try: - update_user_primary_email_address(new_primary) - except TypeError: - message = ( - f'New primary EmailAddress {new_primary} has unexpected type: ' - f'{type(new_primary)}.') - logger.error(message) - messages.error(self.request, self.error_message) - except ValueError: - message = ( - f'New primary EmailAddress {new_primary.pk} for User ' - f'{user.pk} is unexpectedly not verified.') - logger.error(message) - messages.error(self.request, self.error_message) - except Exception as e: - message = ( - f'Encountered unexpected exception when updating User ' - f'{user.pk}\'s primary EmailAddress to {new_primary.pk}. ' - f'Details:') - logger.error(message) - logger.exception(e) - messages.error(self.request, self.error_message) - else: - message = f'{new_primary.email} is your new primary email address.' - messages.success(self.request, message) - - return super().form_valid(form) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['user'] = self.request.user - return kwargs - - def get_success_url(self): - return reverse('user-profile') - - class EmailAddressExistsView(View): def get(self, request, *args, **kwargs): From f51b39d454cadd74db6f8590f807a77a86f6c30a Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Feb 2023 10:39:37 -0800 Subject: [PATCH 05/72] Move helper method for making an address a user's primary --- coldfront/core/account/utils/__init__.py | 0 coldfront/core/account/utils/queries.py | 64 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 coldfront/core/account/utils/__init__.py create mode 100644 coldfront/core/account/utils/queries.py diff --git a/coldfront/core/account/utils/__init__.py b/coldfront/core/account/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/account/utils/queries.py b/coldfront/core/account/utils/queries.py new file mode 100644 index 000000000..7fe087e0d --- /dev/null +++ b/coldfront/core/account/utils/queries.py @@ -0,0 +1,64 @@ +from django.db import transaction + +from allauth.account.models import EmailAddress + +import logging + + +logger = logging.getLogger(__name__) + + +def update_user_primary_email_address(email_address): + """Given an EmailAddress, which must be verified, perform the + following: + - If the user's current email field does not have a + corresponding EmailAddress, create one (verified); + - Set the user's email field to it; + - Set it as the primary EmailAddress of the user; and + - Set the user's other EmailAddress objects to be non-primary. + + Perform the updates in a transaction so that they all fail together + or all succeed together. + + Parameters: + - email_address (EmailAddress): the EmailAddress object to set + as the new primary + + Returns: + - None + + Raises: + - TypeError, if the provided address has an invalid type + - ValueError, if the provided address is not verified + """ + if not isinstance(email_address, EmailAddress): + raise TypeError(f'Invalid EmailAddress {email_address}.') + if not email_address.verified: + raise ValueError(f'EmailAddress {email_address} is unverified.') + + user = email_address.user + with transaction.atomic(): + + old_primary, created = EmailAddress.objects.get_or_create( + user=user, email=user.email.lower()) + if created: + message = ( + f'Created EmailAddress {old_primary.pk} for User {user.pk}\'s ' + f'old primary address {old_primary.email}, which unexpectedly ' + f'did not exist.') + logger.warning(message) + old_primary.verified = True + old_primary.primary = False + old_primary.save() + + # TODO: Hide behind feature flag? This seems relevant no matter what. + for ea in EmailAddress.objects.filter( + user=user, primary=True).exclude(pk=email_address.pk): + ea.primary = False + ea.save() + + user.email = email_address.email + user.save() + + email_address.primary = True + email_address.save() From 928a41a0dbca0ab3d3c038bcd4c804aa51c686ec Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Feb 2023 15:10:23 -0800 Subject: [PATCH 06/72] Replace references to old EmailAddress with new one --- .../test_models => account/tests}/__init__.py | 0 .../core/account/tests/test_admin/__init__.py | 0 .../test_admin/test_email_address_admin.py | 48 +++---- .../core/account/tests/test_utils/__init__.py | 0 .../tests/test_utils/test_queries/__init__.py | 0 .../test_update_user_primary_email_address.py | 52 ++++---- .../load_allocation_renewal_requests.py | 39 +----- coldfront/core/socialaccount/signals.py | 2 +- coldfront/core/user/admin.py | 4 +- coldfront/core/user/auth.py | 9 +- coldfront/core/user/forms.py | 7 +- .../commands/create_email_addresses.py | 25 +--- .../management/commands/lower_email_case.py | 4 +- .../tests/test_models/test_email_address.py | 118 ------------------ .../test_views/test_activate_user_account.py | 32 ++--- .../test_update_primary_emailaddress_view.py | 77 ------------ coldfront/core/user/utils.py | 58 +-------- 17 files changed, 97 insertions(+), 378 deletions(-) rename coldfront/core/{user/tests/test_models => account/tests}/__init__.py (100%) create mode 100644 coldfront/core/account/tests/test_admin/__init__.py rename coldfront/core/{user => account}/tests/test_admin/test_email_address_admin.py (90%) create mode 100644 coldfront/core/account/tests/test_utils/__init__.py create mode 100644 coldfront/core/account/tests/test_utils/test_queries/__init__.py rename coldfront/core/{user/tests/test_utils => account/tests/test_utils/test_queries}/test_update_user_primary_email_address.py (79%) delete mode 100644 coldfront/core/user/tests/test_models/test_email_address.py delete mode 100644 coldfront/core/user/tests/test_views/test_update_primary_emailaddress_view.py diff --git a/coldfront/core/user/tests/test_models/__init__.py b/coldfront/core/account/tests/__init__.py similarity index 100% rename from coldfront/core/user/tests/test_models/__init__.py rename to coldfront/core/account/tests/__init__.py diff --git a/coldfront/core/account/tests/test_admin/__init__.py b/coldfront/core/account/tests/test_admin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/user/tests/test_admin/test_email_address_admin.py b/coldfront/core/account/tests/test_admin/test_email_address_admin.py similarity index 90% rename from coldfront/core/user/tests/test_admin/test_email_address_admin.py rename to coldfront/core/account/tests/test_admin/test_email_address_admin.py index 39ab635f6..1662a7da3 100644 --- a/coldfront/core/user/tests/test_admin/test_email_address_admin.py +++ b/coldfront/core/account/tests/test_admin/test_email_address_admin.py @@ -1,15 +1,17 @@ -from django.core.exceptions import ValidationError -from django.contrib.auth.models import User from django.contrib.admin.sites import AdminSite -from coldfront.core.user.models import EmailAddress -from coldfront.core.user.admin import EmailAddressAdmin -from coldfront.core.user.tests.utils import TestUserBase -from django.http import HttpRequest -from django.contrib.messages.storage import default_storage from django.contrib.messages import get_messages +from django.contrib.messages.storage import default_storage +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.http import HttpRequest + +from allauth.account.models import EmailAddress + +from coldfront.core.account.admin import EmailAddressAdmin +from coldfront.core.user.tests.utils import TestUserBase -class EmailAddressAdminTest(TestUserBase): +class TestEmailAddressAdmin(TestUserBase): """ Class for testing methods in EmailAddressAdmin """ @@ -35,32 +37,32 @@ def setUp(self): self.email1 = EmailAddress.objects.create( user=self.user1, email='email1@email.com', - is_verified=True, - is_primary=True) + verified=True, + primary=True) self.email2 = EmailAddress.objects.create( user=self.user1, email='email2@email.com', - is_verified=True, - is_primary=False) + verified=True, + primary=False) self.email3 = EmailAddress.objects.create( user=self.user2, email='email3@email.com', - is_verified=True, - is_primary=True) + verified=True, + primary=True) self.email4 = EmailAddress.objects.create( user=self.user2, email='email4@email.com', - is_verified=True, - is_primary=False) + verified=True, + primary=False) self.email5 = EmailAddress.objects.create( user=self.user2, email='email5@email.com', - is_verified=False, - is_primary=False) + verified=False, + primary=False) self.request = HttpRequest() setattr(self.request, 'session', 'session') @@ -121,7 +123,7 @@ def test_make_primary_single_primary_email(self): query_set = EmailAddress.objects.filter(pk=self.email1.pk) self.app_admin.make_primary(self.request, query_set) self.email1.refresh_from_db() - self.assertTrue(self.email1.is_primary) + self.assertTrue(self.email1.primary) storage = list(get_messages(self.request)) self.assertEqual(len(storage), 1) @@ -136,10 +138,10 @@ def test_make_primary_single_non_primary_email(self): query_set = EmailAddress.objects.filter(pk=self.email2.pk) self.app_admin.make_primary(self.request, query_set) self.email1.refresh_from_db() - self.assertFalse(self.email1.is_primary) + self.assertFalse(self.email1.primary) self.email2.refresh_from_db() - self.assertTrue(self.email2.is_primary) + self.assertTrue(self.email2.primary) storage = list(get_messages(self.request)) self.assertEqual(len(storage), 1) @@ -152,7 +154,7 @@ def test_make_delete_queryset_only_primary(self): Testing EmailAddressAdmin delete_queryset method with only primary emails """ - query_set = EmailAddress.objects.filter(is_primary=True) + query_set = EmailAddress.objects.filter(primary=True) self.app_admin.delete_queryset(self.request, query_set) self.assertTrue(EmailAddress.objects.filter(pk=self.email1.pk).exists()) @@ -171,7 +173,7 @@ def test_make_delete_queryset_only_non_primary(self): primary emails """ - query_set = EmailAddress.objects.filter(is_primary=False) + query_set = EmailAddress.objects.filter(primary=False) self.app_admin.delete_queryset(self.request, query_set) self.assertFalse(EmailAddress.objects.filter(pk=self.email2.pk).exists()) diff --git a/coldfront/core/account/tests/test_utils/__init__.py b/coldfront/core/account/tests/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/account/tests/test_utils/test_queries/__init__.py b/coldfront/core/account/tests/test_utils/test_queries/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/user/tests/test_utils/test_update_user_primary_email_address.py b/coldfront/core/account/tests/test_utils/test_queries/test_update_user_primary_email_address.py similarity index 79% rename from coldfront/core/user/tests/test_utils/test_update_user_primary_email_address.py rename to coldfront/core/account/tests/test_utils/test_queries/test_update_user_primary_email_address.py index 82c35d490..e1a3240ae 100644 --- a/coldfront/core/user/tests/test_utils/test_update_user_primary_email_address.py +++ b/coldfront/core/account/tests/test_utils/test_queries/test_update_user_primary_email_address.py @@ -1,10 +1,12 @@ -from coldfront.core.user.models import EmailAddress -from coldfront.core.user.tests.utils import TestUserBase -from coldfront.core.user.utils import update_user_primary_email_address from django.contrib.auth.models import User +from allauth.account.models import EmailAddress -class TestUpdateUserPrimaryEmailAddress(TestUserBase): +from coldfront.core.account.utils.queries import update_user_primary_email_address +from coldfront.core.utils.tests.test_base import TestBase + + +class TestUpdateUserPrimaryEmailAddress(TestBase): """A class for testing the utility method update_user_primary_email_address.""" @@ -26,8 +28,8 @@ def test_creates_email_address_for_old_user_email_if_nonexistent(self): new_primary = EmailAddress.objects.create( user=self.user, email='new@email.com', - is_verified=True, - is_primary=False) + verified=True, + primary=False) self.assertEqual(EmailAddress.objects.count(), 1) with self.assertLogs('', 'WARNING') as cm: @@ -41,8 +43,8 @@ def test_creates_email_address_for_old_user_email_if_nonexistent(self): f'An EmailAddress for User {self.user} and email {old_email} ' f'should have been created.') else: - self.assertTrue(email_address.is_verified) - self.assertFalse(email_address.is_primary) + self.assertTrue(email_address.verified) + self.assertFalse(email_address.primary) # Assert that a warning was logged. self.assertEqual(len(cm.output), 1) @@ -65,13 +67,13 @@ def test_raises_type_error_for_bad_input(self): self.fail('A TypeError should have been raised.') def test_raises_value_error_for_unverified_input(self): - """Test that, if the input is an EmailAddress with is_verified + """Test that, if the input is an EmailAddress with verified set to False, a ValueError is raised.""" email_address = EmailAddress.objects.create( user=self.user, email=self.user.email, - is_verified=False, - is_primary=False) + verified=False, + primary=False) try: update_user_primary_email_address(email_address) except ValueError as e: @@ -86,14 +88,14 @@ def test_sets_email_address_to_primary(self): new_primary = EmailAddress.objects.create( user=self.user, email='new@email.com', - is_verified=True, - is_primary=False) - self.assertFalse(new_primary.is_primary) + verified=True, + primary=False) + self.assertFalse(new_primary.primary) update_user_primary_email_address(new_primary) new_primary.refresh_from_db() - self.assertTrue(new_primary.is_primary) + self.assertTrue(new_primary.primary) def test_sets_user_email_field(self): """Test that the method sets the User's "email" field to that of @@ -103,8 +105,8 @@ def test_sets_user_email_field(self): new_primary = EmailAddress.objects.create( user=self.user, email='new@email.com', - is_verified=True, - is_primary=False) + verified=True, + primary=False) update_user_primary_email_address(new_primary) @@ -112,32 +114,32 @@ def test_sets_user_email_field(self): self.assertEqual(self.user.email, new_primary.email) def test_unsets_other_primary_email_addresses(self): - """Test that the method unsets the "is_primary" fields of other + """Test that the method unsets the "primary" fields of other EmailAddresses belonging to the User.""" kwargs = { 'user': self.user, - 'is_verified': True, - 'is_primary': False, + 'verified': True, + 'primary': False, } for i in range(3): kwargs['email'] = f'{i}@email.com' EmailAddress.objects.create(**kwargs) # Bypass the "save" method, which prevents multiple primary addresses, # by using the "update" method. - EmailAddress.objects.filter(user=self.user).update(is_primary=True) + EmailAddress.objects.filter(user=self.user).update(primary=True) user_primary_emails = EmailAddress.objects.filter( - user=self.user, is_primary=True) + user=self.user, primary=True) self.assertEqual(user_primary_emails.count(), 3) new_primary = EmailAddress.objects.create( user=self.user, email='new@email.com', - is_verified=True, - is_primary=False) + verified=True, + primary=False) update_user_primary_email_address(new_primary) user_primary_emails = EmailAddress.objects.filter( - user=self.user, is_primary=True) + user=self.user, primary=True) self.assertEqual(user_primary_emails.count(), 1) self.assertEqual(user_primary_emails.first().pk, new_primary.pk) diff --git a/coldfront/core/allocation/management/commands/load_allocation_renewal_requests.py b/coldfront/core/allocation/management/commands/load_allocation_renewal_requests.py index cf233f6c1..ecc7598d8 100644 --- a/coldfront/core/allocation/management/commands/load_allocation_renewal_requests.py +++ b/coldfront/core/allocation/management/commands/load_allocation_renewal_requests.py @@ -9,7 +9,8 @@ from django.core.management.base import BaseCommand from django.core.management.base import CommandError from django.db import transaction -from django.utils.module_loading import import_string + +from allauth.account.models import EmailAddress from coldfront.core.allocation.models import AllocationPeriod from coldfront.core.allocation.models import AllocationRenewalRequest @@ -40,10 +41,6 @@ class Command(BaseCommand): logger = logging.getLogger(__name__) - def __init__(self, *args, **kwargs): - super().__init__(*args, *kwargs) - self.email_module_dict = {} - def add_arguments(self, parser): parser.add_argument( 'json', @@ -58,15 +55,6 @@ def add_arguments(self, parser): 'allocation_period_name', help='The name of the AllocationPeriod the renewals are under.', type=str) - # TODO: Remove this once all emails are transitioned to - # TODO: allauth.account.models.EmailAddress. - parser.add_argument( - 'email_module', - choices=['allauth.account.models', 'coldfront.core.user.models'], - help=( - 'There are temporarily two EmailAddress models, until all can ' - 'be transitioned under allauth.account.models.'), - type=str) parser.add_argument( '--process', action='store_true', @@ -92,16 +80,6 @@ def handle(self, *args, **options): raise CommandError( f'Invalid AllocationPeriod {allocation_period_name}.') - email_module = options['email_module'] - if email_module == 'allauth.account.models': - verified_field, primary_field = 'verified', 'primary' - else: - verified_field, primary_field = 'is_verified', 'is_primary' - self.email_module_dict['model'] = import_string( - f'{email_module}.EmailAddress') - self.email_module_dict['verified_field'] = verified_field - self.email_module_dict['primary_field'] = primary_field - valid, already_renewed, invalid = self.parse_input_file( file_path, allocation_period) self.process_valid_objects( @@ -347,10 +325,6 @@ def _update_or_create_user_and_email_address(self, email, first_name='', email. If provided (and not already set), set the given first, middle, and last names. Also update UserProfile.is_pi if requested.""" - EmailAddress = self.email_module_dict['model'] - email_verified_field = self.email_module_dict['verified_field'] - email_primary_field = self.email_module_dict['primary_field'] - user = self._get_user_with_email(email) if isinstance(user, User): user.first_name = user.first_name or first_name @@ -368,8 +342,8 @@ def _update_or_create_user_and_email_address(self, email, first_name='', user=user, email=email, defaults={ - email_verified_field: True, - email_primary_field: True}) + 'verified': True, + 'primary': True}) if created: message = ( f'Created EmailAddress {email_address.pk} for User ' @@ -390,8 +364,8 @@ def _update_or_create_user_and_email_address(self, email, first_name='', kwargs = { 'user': user, 'email': email, - email_verified_field: True, - email_primary_field: True, + 'verified': True, + 'primary': True, } email_address = EmailAddress.objects.create(**kwargs) message = ( @@ -418,7 +392,6 @@ def _get_first_middle_last_names(full_name): def _get_user_with_email(self, email): """Return the User associated with the given email, or None.""" - EmailAddress = self.email_module_dict['model'] try: return User.objects.get(email__iexact=email) except User.DoesNotExist: diff --git a/coldfront/core/socialaccount/signals.py b/coldfront/core/socialaccount/signals.py index 25d363c64..662529647 100644 --- a/coldfront/core/socialaccount/signals.py +++ b/coldfront/core/socialaccount/signals.py @@ -6,7 +6,7 @@ from django.db import transaction from django.dispatch import receiver -from allauth.socialaccount.models import EmailAddress +from allauth.account.models import EmailAddress from allauth.socialaccount.models import SocialLogin from allauth.socialaccount.providers.base import AuthProcess from allauth.socialaccount.signals import social_account_added diff --git a/coldfront/core/user/admin.py b/coldfront/core/user/admin.py index 7f4a7c342..40bff30ca 100644 --- a/coldfront/core/user/admin.py +++ b/coldfront/core/user/admin.py @@ -1,6 +1,8 @@ from django.contrib import admin -from coldfront.core.user.models import UserProfile, EmailAddress +from allauth.account.models import EmailAddress + +from coldfront.core.user.models import UserProfile @admin.register(UserProfile) diff --git a/coldfront/core/user/auth.py b/coldfront/core/user/auth.py index 4e1022c0d..f5a79e8d3 100644 --- a/coldfront/core/user/auth.py +++ b/coldfront/core/user/auth.py @@ -1,7 +1,10 @@ -from coldfront.core.user.models import EmailAddress -from coldfront.core.user.utils import send_email_verification_email from django.contrib.auth.backends import BaseBackend from django.contrib.auth.models import User + +from allauth.account.models import EmailAddress + +from coldfront.core.user.utils import send_email_verification_email + import logging @@ -24,7 +27,7 @@ def authenticate(self, request, username=None, password=None, **kwargs): # If the EmailAddress exists, but is not verified, send a verification # email to it. Only do this if the user is already active; otherwise, # a separate account activation email will handle email verification. - if user.is_active and not email_address.is_verified: + if user.is_active and not email_address.verified: try: send_email_verification_email(email_address) except Exception as e: diff --git a/coldfront/core/user/forms.py b/coldfront/core/user/forms.py index ee29a014e..88bc01bff 100644 --- a/coldfront/core/user/forms.py +++ b/coldfront/core/user/forms.py @@ -8,15 +8,16 @@ from django.contrib.auth.models import User from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site -from django.core.exceptions import ImproperlyConfigured from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.html import mark_safe from django.utils.http import urlsafe_base64_encode from django.utils.translation import gettext_lazy as _ +from allauth.account.models import EmailAddress + from coldfront.core.user.utils import send_account_activation_email -from coldfront.core.user.models import UserProfile, EmailAddress +from coldfront.core.user.models import UserProfile from coldfront.core.utils.mail import dummy_email_address from phonenumber_field.formfields import PhoneNumberField @@ -255,7 +256,7 @@ def get_email_address(email): if one exists, else None.""" try: return EmailAddress.objects.select_related('user').get( - email=email, is_verified=True, user__is_active=True) + email=email, verified=True, user__is_active=True) except EmailAddress.DoesNotExist: return None diff --git a/coldfront/core/user/management/commands/create_email_addresses.py b/coldfront/core/user/management/commands/create_email_addresses.py index 357217f14..f5a71f065 100644 --- a/coldfront/core/user/management/commands/create_email_addresses.py +++ b/coldfront/core/user/management/commands/create_email_addresses.py @@ -1,5 +1,8 @@ from django.contrib.auth.models import User from django.core.management.base import BaseCommand + +from allauth.account.models import EmailAddress + import logging @@ -15,27 +18,9 @@ class Command(BaseCommand): 'one-time use for existing users loaded in from spreadsheets.') logger = logging.getLogger(__name__) - def add_arguments(self, parser): - # TODO: Remove this once all emails are transitioned to - # TODO: allauth.account.models.EmailAddress. - parser.add_argument( - 'module', - choices=['allauth.account.models', 'coldfront.core.user.models'], - help=( - 'There are temporarily two EmailAddress models, until all can ' - 'be transitioned under allauth.account.models.'), - type=str) - def handle(self, *args, **options): """For each User that has no EmailAddress, create a verified, primary instance using the User's email field.""" - if options['module'] == 'allauth.account.models': - from allauth.account.models import EmailAddress - verified_field, primary_field = 'verified', 'primary' - else: - from coldfront.core.user.models import EmailAddress - verified_field, primary_field = 'is_verified', 'is_primary' - user_pks_with_emails = EmailAddress.objects.values_list( 'user', flat=True) users_without_emails = User.objects.exclude( @@ -46,8 +31,8 @@ def handle(self, *args, **options): kwargs = { 'user': user, 'email': email, - verified_field: True, - primary_field: True, + 'verified': True, + 'primary': True, } if not email: message = f'User {user.pk} email is empty.' diff --git a/coldfront/core/user/management/commands/lower_email_case.py b/coldfront/core/user/management/commands/lower_email_case.py index c467a5291..521079712 100644 --- a/coldfront/core/user/management/commands/lower_email_case.py +++ b/coldfront/core/user/management/commands/lower_email_case.py @@ -1,6 +1,8 @@ -from coldfront.core.user.models import EmailAddress from django.contrib.auth.models import User from django.core.management.base import BaseCommand + +from allauth.account.models import EmailAddress + import logging diff --git a/coldfront/core/user/tests/test_models/test_email_address.py b/coldfront/core/user/tests/test_models/test_email_address.py deleted file mode 100644 index 43d1e2e1a..000000000 --- a/coldfront/core/user/tests/test_models/test_email_address.py +++ /dev/null @@ -1,118 +0,0 @@ -from coldfront.core.user.models import EmailAddress -from coldfront.core.user.tests.utils import TestUserBase -from django.contrib.auth.models import User -from django.core.exceptions import ValidationError - - -class TestEmailAddress(TestUserBase): - """A class for testing the EmailAddress model.""" - - def setUp(self): - """Set up test data.""" - super().setUp() - - self.user = User.objects.create( - email='user@email.com', - first_name='First', - last_name='Last', - username='user') - - def test_save_nonexistent_to_primary(self): - """Test that saving an EmailAddress as a primary one when it did - not previously exist succeeds.""" - kwargs = { - 'user': self.user, - 'email': self.user.email, - 'is_verified': True, - 'is_primary': True, - } - try: - email_address = EmailAddress.objects.create(**kwargs) - except ValidationError: - self.fail('A ValidationError should not have been raised.') - self.assertTrue(email_address.is_primary) - - def test_save_non_primary_to_primary_no_others(self): - """Test that saving an EmailAddress that was not previously - primary as primary, when there are no other primary - EmailAddresses, succeeds.""" - kwargs = { - 'user': self.user, - 'email': self.user.email, - 'is_verified': True, - 'is_primary': False, - } - try: - email_address = EmailAddress.objects.create(**kwargs) - except ValidationError: - self.fail('A ValidationError should not have been raised.') - self.assertFalse(email_address.is_primary) - - try: - email_address.is_primary = True - email_address.save() - except ValidationError: - self.fail('A ValidationError should not have been raised.') - self.assertTrue(email_address.is_primary) - - def test_save_non_primary_to_primary_others(self): - """Test that saving an EmailAddress that was not previously - primary as primary, when there are other primary - EmailAddresses, fails.""" - kwargs = { - 'user': self.user, - 'email': self.user.email, - 'is_verified': True, - 'is_primary': True, - } - EmailAddress.objects.create(**kwargs) - - kwargs['email'] = 'new@email.com' - try: - EmailAddress.objects.create(**kwargs) - except ValidationError as e: - self.assertIn('User already has a primary email address', str(e)) - else: - self.fail('A ValidationError should have been raised.') - - def test_save_unverified_as_primary(self): - """Test that saving an unverified EmailAddress as primary - fails.""" - kwargs = { - 'user': self.user, - 'email': self.user.email, - 'is_verified': False, - 'is_primary': True, - } - try: - EmailAddress.objects.create(**kwargs) - except ValidationError as e: - self.assertIn( - 'Only verified emails may be set to primary.', str(e)) - else: - self.fail('A ValidationError should have been raised.') - - def test_save_updates_user_email_field_if_primary(self): - """Test that saving a primary EmailAddress updates the "email" - field of the User.""" - self.assertEqual(self.user.email, 'user@email.com') - - # The field should be updated if the address goes from non-primary to - # primary. - kwargs = { - 'user': self.user, - 'email': 'new0@email.com', - 'is_verified': True, - 'is_primary': True, - } - email_address = EmailAddress.objects.create(**kwargs) - self.user.refresh_from_db() - self.assertEqual(self.user.email, kwargs['email']) - - # The field should be updated if the address goes from primary to - # primary. - kwargs['email'] = 'new1@email.com' - email_address.email = kwargs['email'] - email_address.save() - self.user.refresh_from_db() - self.assertEqual(self.user.email, kwargs['email']) diff --git a/coldfront/core/user/tests/test_views/test_activate_user_account.py b/coldfront/core/user/tests/test_views/test_activate_user_account.py index 1124edc2a..8f7080475 100644 --- a/coldfront/core/user/tests/test_views/test_activate_user_account.py +++ b/coldfront/core/user/tests/test_views/test_activate_user_account.py @@ -1,12 +1,12 @@ +from django.contrib.auth.models import User +from django.contrib.messages import get_messages +from django.test import Client + +from allauth.account.models import EmailAddress from flags.state import enable_flag -from coldfront.core.user.models import EmailAddress from coldfront.core.user.tests.utils import TestUserBase from coldfront.core.user.utils import account_activation_url -from django.contrib.auth.models import User -from django.contrib.messages import get_messages -from django.test import Client -from django.urls import reverse class TestActivateUserAccount(TestUserBase): @@ -56,8 +56,8 @@ def test_creates_verified_primary_email_address(self): email_address = EmailAddress.objects.get(email=self.user.email) self.assertEqual(email_address.user, self.user) - self.assertTrue(email_address.is_verified) - self.assertTrue(email_address.is_primary) + self.assertTrue(email_address.verified) + self.assertTrue(email_address.primary) def test_updates_existing_email_addresses(self): """Test that account activation updates EmailAddresses so that @@ -65,18 +65,18 @@ def test_updates_existing_email_addresses(self): # Create an unverified, non-primary EmailAddress for the User. kwargs = { 'user': self.user, - 'is_verified': False, - 'is_primary': False, + 'verified': False, + 'primary': False, } email_address = EmailAddress.objects.create( user=self.user, email=self.user.email, - is_verified=False, - is_primary=False) + verified=False, + primary=False) # Create other primary EmailAddresses. other_email_addresses = [] - kwargs['is_verified'] = True + kwargs['verified'] = True for i in range(3): kwargs['email'] = f'{i}@email.com' other_email_addresses.append( @@ -85,7 +85,7 @@ def test_updates_existing_email_addresses(self): # by using the "update" method. EmailAddress.objects.filter( pk__in=[ea.pk for ea in other_email_addresses]).update( - is_primary=True) + primary=True) url = account_activation_url(self.user) response = self.client.get(url) @@ -95,11 +95,11 @@ def test_updates_existing_email_addresses(self): email_address.refresh_from_db() self.assertEqual(email_address.user, self.user) - self.assertTrue(email_address.is_verified) - self.assertTrue(email_address.is_primary) + self.assertTrue(email_address.verified) + self.assertTrue(email_address.primary) for ea in other_email_addresses: ea.refresh_from_db() - self.assertFalse(ea.is_primary) + self.assertFalse(ea.primary) # TODO diff --git a/coldfront/core/user/tests/test_views/test_update_primary_emailaddress_view.py b/coldfront/core/user/tests/test_views/test_update_primary_emailaddress_view.py deleted file mode 100644 index 94a7ac9a4..000000000 --- a/coldfront/core/user/tests/test_views/test_update_primary_emailaddress_view.py +++ /dev/null @@ -1,77 +0,0 @@ -from flags.state import enable_flag - -from coldfront.core.user.models import EmailAddress -from coldfront.core.user.tests.utils import TestUserBase -from django.contrib.auth.models import User -from django.contrib.messages import get_messages -from django.test import Client -from django.urls import reverse - - -class TestUpdateUserPrimaryEmailAddress(TestUserBase): - """ - A class for testing the view for updating a user's - primary email address'. - """ - - def setUp(self): - """Set up test data.""" - enable_flag('BASIC_AUTH_ENABLED') - super().setUp() - - self.password = 'password' - - self.user1 = User.objects.create( - email='user1@email.com', - first_name='First', - last_name='Last', - username='user1') - self.user1.set_password(self.password) - self.user1.save() - - self.email1 = EmailAddress.objects.create( - user=self.user1, - email='email1@email.com', - is_verified=True, - is_primary=True) - - self.email2 = EmailAddress.objects.create( - user=self.user1, - email='email2@email.com', - is_verified=True, - is_primary=False) - - self.client = Client() - - def test_update_primary_emailaddress_view(self): - """ - Testing UpdatePrimaryEmailAddressView - """ - - self.client.login(username=self.user1.username, password=self.password) - url = reverse( - 'update-primary-email-address') - data = {'email_address': str(self.email2.pk)} - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, f'Your current primary email address ' - f'is: {self.email1.email}.') - self.assertContains(response, self.email2.email) - - response = self.client.post(url, data) - self.assertEqual(response.status_code, 302) - - self.email1.refresh_from_db() - self.email2.refresh_from_db() - - self.assertTrue(self.email2.is_primary) - self.assertFalse(self.email1.is_primary) - - self.assertRedirects(response, reverse('user-profile')) - - messages = [str(m) for m in get_messages(response.wsgi_request)] - self.assertTrue(len(messages), 1) - self.assertEqual(messages[0], f'{self.email2.email} is your new ' - f'primary email address.') - - self.client.logout() diff --git a/coldfront/core/user/utils.py b/coldfront/core/user/utils.py index cc0ab43a8..47bede5d1 100644 --- a/coldfront/core/user/utils.py +++ b/coldfront/core/user/utils.py @@ -7,7 +7,6 @@ from django.conf import settings from django.contrib.auth.models import User from django.contrib.auth.tokens import PasswordResetTokenGenerator -from django.db import transaction from django.db.models import Q from django.urls import reverse from django.utils.crypto import constant_time_compare @@ -16,7 +15,6 @@ from django.utils.http import urlsafe_base64_encode from django.utils.module_loading import import_string -from coldfront.core.user.models import EmailAddress from coldfront.core.utils.common import import_from_settings from coldfront.core.utils.mail import send_email_template @@ -24,6 +22,7 @@ logger = logging.getLogger(__name__) + class UserSearch(abc.ABC): def __init__(self, user_search_string, search_by): @@ -285,58 +284,3 @@ def send_email_verification_email(email_address): receiver_list = [email_address.email, ] send_email_template(subject, template_name, context, sender, receiver_list) - - -def update_user_primary_email_address(email_address): - """Given an EmailAddress, which must be verified, perform the - following: - - If the user's current email field does not have a - corresponding EmailAddress, create one (verified); - - Set the user's email field to it; - - Set it as the primary EmailAddress of the user; and - - Set the user's other EmailAddress objects to be non-primary. - - Perform the updates in a transaction so that they all fail together - or all succeed together. - - Parameters: - - email_address (EmailAddress): the EmailAddress object to set - as the new primary - - Returns: - - None - - Raises: - - TypeError, if the provided address has an invalid type - - ValueError, if the provided address is not verified - """ - if not isinstance(email_address, EmailAddress): - raise TypeError(f'Invalid EmailAddress {email_address}.') - if not email_address.is_verified: - raise ValueError(f'EmailAddress {email_address} is unverified.') - - user = email_address.user - with transaction.atomic(): - - old_primary, created = EmailAddress.objects.get_or_create( - user=user, email=user.email.lower()) - if created: - message = ( - f'Created EmailAddress {old_primary.pk} for User {user.pk}\'s ' - f'old primary address {old_primary.email}, which unexpectedly ' - f'did not exist.') - logger.warning(message) - old_primary.is_verified = True - old_primary.is_primary = False - old_primary.save() - - for ea in EmailAddress.objects.filter( - user=user, is_primary=True).exclude(pk=email_address.pk): - ea.is_primary = False - ea.save() - - user.email = email_address.email - user.save() - - email_address.is_primary = True - email_address.save() From accc27f3bfa01e860dc43b2aef9275753e46c35d Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Thu, 9 Feb 2023 16:15:28 -0800 Subject: [PATCH 07/72] Create second version of SSO login page for BRC, with CalNet login prioritized; make login button text on home page dynamic --- .../templates/portal/nonauthorized_home.html | 5 + .../user/templates/user/sso_login_brc.html | 122 ++++++++++++++++++ .../{sso_login.html => sso_login_lrc.html} | 0 coldfront/core/user/views.py | 11 +- 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 coldfront/core/user/templates/user/sso_login_brc.html rename coldfront/core/user/templates/user/{sso_login.html => sso_login_lrc.html} (100%) diff --git a/coldfront/core/portal/templates/portal/nonauthorized_home.html b/coldfront/core/portal/templates/portal/nonauthorized_home.html index dbea8e85a..c0835f63f 100644 --- a/coldfront/core/portal/templates/portal/nonauthorized_home.html +++ b/coldfront/core/portal/templates/portal/nonauthorized_home.html @@ -38,7 +38,12 @@

Welcome to {{ PORTAL_NAME }}

{% if sso_enabled %} + {% flag_enabled 'BRC_ONLY' as brc_only %} + {% if brc_only %} + CalNet: Log In + {% else %} Berkeley Lab: Log In + {% endif %} diff --git a/coldfront/core/user/templates/user/sso_login_brc.html b/coldfront/core/user/templates/user/sso_login_brc.html new file mode 100644 index 000000000..94a8a2717 --- /dev/null +++ b/coldfront/core/user/templates/user/sso_login_brc.html @@ -0,0 +1,122 @@ +{% extends "common/base.html" %} +{% load feature_flags %} +{% load socialaccount %} + +{% load common_tags %} +{% load static %} + +{% block title %} +Log In +{% endblock %} + +{% block content %} + +

Log In: I am a...

+ +
+ +
+
+
+ berkeley_lab_logo +
+
+

Berkeley Lab Collaborator

+

+ I do not have a CalNet ID, but I do have a Berkeley Lab Identity. +

+

+ + + Log In + +

+
+
+
+
+
+
+
+ cilogon_logo +
+
+

External Collaborator

+

+ I do not have a CalNet ID, and I do not have a Berkeley Lab Identity. +

+

+ + + Log In + +

+
+
+
+
+
+ +
+
+ +
+
+
    +
  • + Authentication is managed by + CILogon. You will be + redirected there, where you will select a provider to authenticate + with (University of California, Berkeley for most users). +
  • +
  • + If you have a CalNet ID, please authenticate using the first option + above. UC Berkeley users who do not use their CalNet ID may + experience delays when requesting cluster access. +
  • +
  • + External collaborators should select the provider of their home + institution. If your provider is not listed, select the + Google provider. +
  • +
+
+
+
+
+ +{% endblock %} + + +{% block javascript %} +{{ block.super }} + +{% endblock %} diff --git a/coldfront/core/user/templates/user/sso_login.html b/coldfront/core/user/templates/user/sso_login_lrc.html similarity index 100% rename from coldfront/core/user/templates/user/sso_login.html rename to coldfront/core/user/templates/user/sso_login_lrc.html diff --git a/coldfront/core/user/views.py b/coldfront/core/user/views.py index 4faff26e0..3b8301ccf 100644 --- a/coldfront/core/user/views.py +++ b/coldfront/core/user/views.py @@ -544,13 +544,22 @@ def dispatch(self, request, *args, **kwargs): class SSOLoginView(TemplateView): """Display the template for SSO login. If the user is authenticated, redirect to the home page.""" - template_name = 'user/sso_login.html' def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: return redirect(reverse('home')) return super().dispatch(request, *args, **kwargs) + def get_template_names(self): + if flag_enabled('BRC_ONLY'): + return ['user/sso_login_brc.html'] + elif flag_enabled('LRC_ONLY'): + return ['user/sso_login_lrc.html'] + else: + raise ImproperlyConfigured( + 'One of the following flags must be enabled: BRC_ONLY, ' + 'LRC_ONLY.') + class UserRegistrationView(CreateView): From ba675c4b1a71d966275d5d476fa98c1f77867395 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Feb 2023 13:51:53 -0800 Subject: [PATCH 08/72] Add first-pass implementation of short-lived link authentication --- coldfront/config/local_settings.py.sample | 11 +++ coldfront/core/user/forms_/__init__.py | 0 .../core/user/forms_/link_login_forms.py | 10 ++ .../templates/user/request_login_link.html | 40 ++++++++ coldfront/core/user/urls.py | 13 ++- coldfront/core/user/utils.py | 32 ++++++ .../core/user/views_/link_login_views.py | 99 +++++++++++++++++++ coldfront/templates/email/login_link.txt | 14 +++ requirements.txt | 1 + 9 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 coldfront/core/user/forms_/__init__.py create mode 100644 coldfront/core/user/forms_/link_login_forms.py create mode 100644 coldfront/core/user/templates/user/request_login_link.html create mode 100644 coldfront/core/user/views_/link_login_views.py create mode 100644 coldfront/templates/email/login_link.txt diff --git a/coldfront/config/local_settings.py.sample b/coldfront/config/local_settings.py.sample index d5164bebb..7df9a119e 100644 --- a/coldfront/config/local_settings.py.sample +++ b/coldfront/config/local_settings.py.sample @@ -438,6 +438,17 @@ SOCIALACCOUNT_PROVIDERS = { # Always request the 'email' scope. SOCIALACCOUNT_QUERY_EMAIL = True +#------------------------------------------------------------------------------ +# Django Sesame settings +#------------------------------------------------------------------------------ + +EXTRA_AUTHENTICATION_BACKENDS += [ + 'sesame.backends.ModelBackend', +] + +# The number of seconds a login token is valid for. +SESAME_MAX_AGE = 300 + #------------------------------------------------------------------------------ # Data import settings #------------------------------------------------------------------------------ diff --git a/coldfront/core/user/forms_/__init__.py b/coldfront/core/user/forms_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/user/forms_/link_login_forms.py b/coldfront/core/user/forms_/link_login_forms.py new file mode 100644 index 000000000..b5367eb62 --- /dev/null +++ b/coldfront/core/user/forms_/link_login_forms.py @@ -0,0 +1,10 @@ +from django import forms + + +class RequestLoginLinkForm(forms.Form): + + email = forms.EmailField() + + def clean_email(self): + email = self.cleaned_data['email'] + return email.lower() diff --git a/coldfront/core/user/templates/user/request_login_link.html b/coldfront/core/user/templates/user/request_login_link.html new file mode 100644 index 000000000..a94405a47 --- /dev/null +++ b/coldfront/core/user/templates/user/request_login_link.html @@ -0,0 +1,40 @@ +{% extends "common/base.html" %} +{% load static %} +{% load common_tags %} +{% load crispy_forms_tags %} + +{% block title %} +Request Login Link +{% endblock %} + +{% block content %} + +
+ +
+
+ + Request Login Link +
+
+

+ If your institution is not listed in CILogon, you may use this form to + request a short-lived link to log in to the portal. +

+

+ All new users must select an institution listed in CILogon. +

+
+ {% csrf_token %} +

+ {{ form|crispy }} + +

+
+
+
+
+{% endblock %} diff --git a/coldfront/core/user/urls.py b/coldfront/core/user/urls.py index 265c816a4..81edea4f7 100644 --- a/coldfront/core/user/urls.py +++ b/coldfront/core/user/urls.py @@ -5,11 +5,11 @@ from django.contrib.auth.views import PasswordResetDoneView from django.contrib.auth.views import PasswordResetView from django.urls import path, reverse_lazy -from django.views.generic import TemplateView from flags.urls import flagged_paths import coldfront.core.user.views as user_views +import coldfront.core.user.views_.link_login_views as link_login_views import coldfront.core.user.views_.request_hub_views as request_hub_views from coldfront.core.user.forms import VerifiedEmailAddressPasswordResetForm from coldfront.core.user.forms import UserLoginForm @@ -81,6 +81,17 @@ ] +with flagged_paths('LINK_LOGIN_ENABLED') as f_path: + urlpatterns += [ + f_path('request-login-link/', + link_login_views.RequestLoginLinkView.as_view(), + name='request-login-link'), + f_path('link-login/', + link_login_views.LinkLoginView.as_view(), + name='link-login'), + ] + + with flagged_paths('SSO_ENABLED') as f_path: urlpatterns += [ f_path('sso_login/', diff --git a/coldfront/core/user/utils.py b/coldfront/core/user/utils.py index 47bede5d1..2e3e45312 100644 --- a/coldfront/core/user/utils.py +++ b/coldfront/core/user/utils.py @@ -18,6 +18,7 @@ from coldfront.core.utils.common import import_from_settings from coldfront.core.utils.mail import send_email_template +from sesame.utils import get_query_string from urllib.parse import urljoin logger = logging.getLogger(__name__) @@ -211,6 +212,13 @@ def __email_verification_url(email_address): return urljoin(domain, view) +def login_token_url(user): + """Return a Django Sesame login link for the given User.""" + domain = import_from_settings('CENTER_BASE_URL') + path = reverse('link-login') + get_query_string(user) + return urljoin(domain, path) + + def send_account_activation_email(user): """Send an activation email to the given User, who has just created an account, providing a link to activate the account.""" @@ -284,3 +292,27 @@ def send_email_verification_email(email_address): receiver_list = [email_address.email, ] send_email_template(subject, template_name, context, sender, receiver_list) + + +def send_login_link_email(email_address): + """Send an email containing a login link to the given + EmailAddress.""" + email_enabled = import_from_settings('EMAIL_ENABLED', False) + if not email_enabled: + return + + subject = 'Login Link' + template_name = 'email/login_link.txt' + context = { + 'PORTAL_NAME': settings.PORTAL_NAME, + 'center_name': import_from_settings('CENTER_NAME', ''), + 'login_url': login_token_url(email_address.user), + 'login_link_max_age_minutes': ( + import_from_settings('SESAME_MAX_AGE') // 60), + 'signature': import_from_settings('EMAIL_SIGNATURE', ''), + } + + sender = import_from_settings('EMAIL_SENDER') + receiver_list = [email_address.email, ] + + send_email_template(subject, template_name, context, sender, receiver_list) diff --git a/coldfront/core/user/views_/link_login_views.py b/coldfront/core/user/views_/link_login_views.py new file mode 100644 index 000000000..f771e63ee --- /dev/null +++ b/coldfront/core/user/views_/link_login_views.py @@ -0,0 +1,99 @@ +import logging + +from django.contrib import messages +from django.shortcuts import redirect +from django.urls import reverse +from django.views.generic.edit import FormView + +from allauth.account.models import EmailAddress +from sesame.views import LoginView + +from coldfront.core.user.forms_.link_login_forms import RequestLoginLinkForm +from coldfront.core.user.utils import send_login_link_email +from coldfront.core.utils.common import import_from_settings + + +logger = logging.getLogger(__name__) + + +class RequestLoginLinkView(FormView): + """A view that sends a login link to the user with the provided + email address, if any.""" + + form_class = RequestLoginLinkForm + template_name = 'user/request_login_link.html' + + def dispatch(self, request, *args, **kwargs): + if request.user.is_authenticated: + return redirect(reverse('home')) + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + """If the email address belongs to a user, send a login link to + it.""" + email = form.cleaned_data.get('email') + email_address = self._validate_email_address(email) + if email_address: + send_login_link_email(email_address) + self._send_success_message() + return super().form_valid(form) + + def get_success_url(self): + return reverse('request-login-link') + + def _send_success_message(self): + """Send a success message to the user explaining that a link was + (conditionally) sent.""" + login_link_max_age_minutes = ( + import_from_settings('SESAME_MAX_AGE') // 60) + message = ( + f'If the email address you entered corresponds to an existing ' + f'user, please check the address for a login link. Note that this ' + f'link will expire in {login_link_max_age_minutes} minutes.') + messages.success(self.request, message) + + def _validate_email_address(self, email): + """Return an EmailAddress object corresponding to the given + address (str) if one exists. Otherwise, return None. Write user + and log messages as needed.""" + email_address = None + try: + email_address = EmailAddress.objects.get(email=email) + except EmailAddress.DoesNotExist: + pass + except EmailAddress.MultipleObjectsReturned: + logger.error( + f'Unexpectedly found multiple EmailAddresses for email ' + f'{email}.') + message = ( + 'Unexpected server error. Please contact an administrator.') + messages.error(self.request, message) + return email_address + + +class LinkLoginView(LoginView): + """A subclass of Django Sesame's login view, with custom logic.""" + + def login_failed(self): + """Send an error message to the user and write to the log before + deferring to parent logic.""" + message = 'Invalid or expired login link.' + messages.error(self.request, message) + logger.warning( + 'A user failed to log in using an invalid or expired login link.') + return super().login_failed() + + def login_success(self): + """Activate the user if needed, send a success message to the + user, and write to the log before deferring to parent logic.""" + user = self.request.user + if not user.is_active: + user.is_active = True + user.save() + + message = f'Successfully signed in as {user.username}.' + messages.success(self.request, message) + logger.warning( + f'User {user.pk} ({user.username}) logged in using a login link.') + + return super().login_success() diff --git a/coldfront/templates/email/login_link.txt b/coldfront/templates/email/login_link.txt new file mode 100644 index 000000000..f42781534 --- /dev/null +++ b/coldfront/templates/email/login_link.txt @@ -0,0 +1,14 @@ +Dear {{ PORTAL_NAME }} user, + +You requested a link to log in to the portal. + +Below is the link, which will expire in {{ login_link_max_age_minutes }} minutes. + +{{ login_url }} + +Do not share this link with anyone else. + +Reminder: If your institution is listed in CILogon, you should use it to log in. + +Thank you, +{{ signature }} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 20420398e..1b118b503 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ django-model-utils==4.1.1 django-phonenumber-field==5.1.0 django-picklefield==2.0 django-q==1.0.1 +django-sesame==3.1 django-settings-export==1.2.1 django-simple-history==2.12.0 django-sslserver==0.20 From a52441387633ff9f992147dbc897d295a0134b68 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Feb 2023 13:52:29 -0800 Subject: [PATCH 09/72] Link to view for requesting login link in SSO login page --- coldfront/core/user/templates/user/sso_login_brc.html | 8 ++++++++ coldfront/core/user/templates/user/sso_login_lrc.html | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/coldfront/core/user/templates/user/sso_login_brc.html b/coldfront/core/user/templates/user/sso_login_brc.html index 94a8a2717..5c32aa3f0 100644 --- a/coldfront/core/user/templates/user/sso_login_brc.html +++ b/coldfront/core/user/templates/user/sso_login_brc.html @@ -104,6 +104,14 @@

External Collaborator

institution. If your provider is not listed, select the Google provider. + {% flag_enabled 'LINK_LOGIN_ENABLED' as link_login_enabled %} + {% if link_login_enabled %} +
  • + If you are an existing user whose institution is not listed, you + may request a short-lived login link + here. +
  • + {% endif %}
    diff --git a/coldfront/core/user/templates/user/sso_login_lrc.html b/coldfront/core/user/templates/user/sso_login_lrc.html index d1f18fb02..f181229b9 100644 --- a/coldfront/core/user/templates/user/sso_login_lrc.html +++ b/coldfront/core/user/templates/user/sso_login_lrc.html @@ -104,6 +104,14 @@

    External Collaborator

    institution. If your provider is not listed, select the Google provider. + {% flag_enabled 'LINK_LOGIN_ENABLED' as link_login_enabled %} + {% if link_login_enabled %} +
  • + If you are an existing user whose institution is not listed, you + may request a short-lived login link + here. +
  • + {% endif %} From ec4a8fc810e06e1e3c955686e6dad6ea488fbcd4 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Feb 2023 14:24:08 -0800 Subject: [PATCH 10/72] Update wording --- .../core/user/templates/user/request_login_link.html | 7 ++++--- coldfront/core/user/templates/user/sso_login_brc.html | 10 +++++----- coldfront/core/user/templates/user/sso_login_lrc.html | 10 +++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/coldfront/core/user/templates/user/request_login_link.html b/coldfront/core/user/templates/user/request_login_link.html index a94405a47..0599d9a2f 100644 --- a/coldfront/core/user/templates/user/request_login_link.html +++ b/coldfront/core/user/templates/user/request_login_link.html @@ -18,11 +18,12 @@

    - If your institution is not listed in CILogon, you may use this form to - request a short-lived link to log in to the portal. + If you have previously used BRC resources, but your institution is not + listed in CILogon, you may use this form to request a short-lived link + to log in to the portal.

    - All new users must select an institution listed in CILogon. + New users must select an institution listed in CILogon.

    {% csrf_token %} diff --git a/coldfront/core/user/templates/user/sso_login_brc.html b/coldfront/core/user/templates/user/sso_login_brc.html index 5c32aa3f0..83430a5a8 100644 --- a/coldfront/core/user/templates/user/sso_login_brc.html +++ b/coldfront/core/user/templates/user/sso_login_brc.html @@ -100,15 +100,15 @@

    External Collaborator

    experience delays when requesting cluster access.
  • - External collaborators should select the provider of their home - institution. If your provider is not listed, select the - Google provider. + External collaborators should select their home institution as the + provider. If you are a new user and your institution is not listed, + select the Google provider.
  • {% flag_enabled 'LINK_LOGIN_ENABLED' as link_login_enabled %} {% if link_login_enabled %}
  • - If you are an existing user whose institution is not listed, you - may request a short-lived login link + If you are an existing external collaborator whose institution is + not listed, you may request a short-lived login link here.
  • {% endif %} diff --git a/coldfront/core/user/templates/user/sso_login_lrc.html b/coldfront/core/user/templates/user/sso_login_lrc.html index f181229b9..d8d97bd97 100644 --- a/coldfront/core/user/templates/user/sso_login_lrc.html +++ b/coldfront/core/user/templates/user/sso_login_lrc.html @@ -100,15 +100,15 @@

    External Collaborator

    experience delays when requesting cluster access.
  • - External collaborators should select the provider of their home - institution. If your provider is not listed, select the - Google provider. + External collaborators should select their home institution as the + provider. If you are a new user and your institution is not listed, + select the Google provider.
  • {% flag_enabled 'LINK_LOGIN_ENABLED' as link_login_enabled %} {% if link_login_enabled %}
  • - If you are an existing user whose institution is not listed, you - may request a short-lived login link + If you are an existing external collaborator whose institution is + not listed, you may request a short-lived login link here.
  • {% endif %} From 62906dad1260838c6212cccb3c942f15dfe554f1 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 15 Feb 2023 14:30:40 -0800 Subject: [PATCH 11/72] Update wording --- coldfront/core/user/templates/user/request_login_link.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coldfront/core/user/templates/user/request_login_link.html b/coldfront/core/user/templates/user/request_login_link.html index 0599d9a2f..8cf24dd0c 100644 --- a/coldfront/core/user/templates/user/request_login_link.html +++ b/coldfront/core/user/templates/user/request_login_link.html @@ -18,9 +18,9 @@

    - If you have previously used BRC resources, but your institution is not - listed in CILogon, you may use this form to request a short-lived link - to log in to the portal. + If you are an existing user, but your institution is not listed in + CILogon, you may use this form to request a short-lived link to log in + to the portal.

    New users must select an institution listed in CILogon. From 4d7e409675cd2345425e8414ec0bfa7d0a6be0d6 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 8 Mar 2023 14:44:56 -0800 Subject: [PATCH 12/72] Correct typo in PI role name: 'Principle' -> 'Principal' in request hub; refactor sorting icons out into template that can be included and optionally hidden; hide it in the request hub --- .../project_removal_request_list_table.html | 19 +++++++------------ .../core/user/views_/request_hub_views.py | 13 +++++++------ coldfront/templates/common/table_sorter.html | 8 ++++++++ 3 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 coldfront/templates/common/table_sorter.html diff --git a/coldfront/core/project/templates/project/project_removal/project_removal_request_list_table.html b/coldfront/core/project/templates/project/project_removal/project_removal_request_list_table.html index 909ed5808..de61baf87 100644 --- a/coldfront/core/project/templates/project/project_removal/project_removal_request_list_table.html +++ b/coldfront/core/project/templates/project/project_removal/project_removal_request_list_table.html @@ -3,37 +3,32 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} {% if request_filter == 'pending' or adj == 'pending' %} Date Requested + {% include 'common/table_sorter.html' with table_sorter_field='request_time' %} {% else %} Date Completed + {% include 'common/table_sorter.html' with table_sorter_field='completion_time' %} {% endif %} - - User Email - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__user__email' %} User - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__user__username' %} Requester - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__username' %} Project - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__project__name' %} Status diff --git a/coldfront/core/user/views_/request_hub_views.py b/coldfront/core/user/views_/request_hub_views.py index 85dc6e94e..5f36368fd 100644 --- a/coldfront/core/user/views_/request_hub_views.py +++ b/coldfront/core/user/views_/request_hub_views.py @@ -406,7 +406,7 @@ def get_secure_dir_join_request(self): request_pks = [request.pk for request in secure_dir_join_pending if request.allocation.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -416,7 +416,7 @@ def get_secure_dir_join_request(self): request_pks = [request.pk for request in secure_dir_join_complete if request.allocation.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -469,7 +469,7 @@ def get_secure_dir_remove_request(self): request_pks = [request.pk for request in secure_dir_remove_pending if request.allocation.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -479,7 +479,7 @@ def get_secure_dir_remove_request(self): request_pks = [request.pk for request in secure_dir_remove_complete if request.allocation.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -533,7 +533,7 @@ def get_secure_dir_request(self): request_pks = [request.pk for request in secure_dir_pending if request.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -543,7 +543,7 @@ def get_secure_dir_request(self): request_pks = [request.pk for request in secure_dir_complete if request.project.projectuser_set.filter( user=user, - role__name='Principle Investigator', + role__name='Principal Investigator', status__name='Active' ).exists()] pi_cond = Q(pk__in=request_pks) @@ -604,5 +604,6 @@ def get_context_data(self, **kwargs): context['admin_staff'] = (self.request.user.is_superuser or self.request.user.is_staff) + context['hide_table_sorter'] = True return context diff --git a/coldfront/templates/common/table_sorter.html b/coldfront/templates/common/table_sorter.html new file mode 100644 index 000000000..907e21d04 --- /dev/null +++ b/coldfront/templates/common/table_sorter.html @@ -0,0 +1,8 @@ +{% if not hide_table_sorter %} + + + + + + +{% endif %} From c43eb04090f374b72cf0179ac3149a8abe912c7c Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Sat, 11 Mar 2023 22:49:20 -0800 Subject: [PATCH 13/72] complete tabbed ui --- .../templates/project/project_detail.html | 2 +- .../savio/project_request_surveys_modal.html | 73 +++++++++++++++++++ coldfront/core/project/views.py | 4 + 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html diff --git a/coldfront/core/project/templates/project/project_detail.html b/coldfront/core/project/templates/project/project_detail.html index f36a836b8..31d8a0ac4 100644 --- a/coldfront/core/project/templates/project/project_detail.html +++ b/coldfront/core/project/templates/project/project_detail.html @@ -475,5 +475,5 @@

    -{% include 'project/project_request/savio/project_request_survey_modal.html' with survey_form=survey_answers %} +{% include 'project/project_request/savio/project_request_surveys_modal.html' with survey_answers=survey_answers %} {% endblock %} diff --git a/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html b/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html new file mode 100644 index 000000000..55f78cd36 --- /dev/null +++ b/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html @@ -0,0 +1,73 @@ +{% load crispy_forms_tags %} + + + + diff --git a/coldfront/core/project/views.py b/coldfront/core/project/views.py index 3c3037231..91da52edc 100644 --- a/coldfront/core/project/views.py +++ b/coldfront/core/project/views.py @@ -278,6 +278,10 @@ def get_context_data(self, **kwargs): context['survey_answers'] = SavioProjectSurveyForm( initial=allocation_request.survey_answers, disable_fields=True) + allocation_requests = SavioProjectAllocationRequest.objects.filter(project=self.object) + survey_answers_list = list(map(lambda x: SavioProjectSurveyForm(initial=x.survey_answers, disable_fields=True), allocation_requests)) + context['survey_answers'] = list(zip(allocation_requests, survey_answers_list)) + context['user_agreement_signed'] = \ access_agreement_signed(self.request.user) From b7b1dd388979da2756dbd84519677ece9bf207aa Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 22 Mar 2023 10:09:29 -0700 Subject: [PATCH 14/72] Disallow superusers, staff from logging in using a link for security reasons --- coldfront/core/user/utils.py | 22 ++++++++++++++++++- .../core/user/views_/link_login_views.py | 22 ++++++++++++++++++- .../templates/email/login_link_ineligible.txt | 12 ++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 coldfront/templates/email/login_link_ineligible.txt diff --git a/coldfront/core/user/utils.py b/coldfront/core/user/utils.py index 2e3e45312..2f3484de5 100644 --- a/coldfront/core/user/utils.py +++ b/coldfront/core/user/utils.py @@ -305,7 +305,6 @@ def send_login_link_email(email_address): template_name = 'email/login_link.txt' context = { 'PORTAL_NAME': settings.PORTAL_NAME, - 'center_name': import_from_settings('CENTER_NAME', ''), 'login_url': login_token_url(email_address.user), 'login_link_max_age_minutes': ( import_from_settings('SESAME_MAX_AGE') // 60), @@ -316,3 +315,24 @@ def send_login_link_email(email_address): receiver_list = [email_address.email, ] send_email_template(subject, template_name, context, sender, receiver_list) + + +def send_login_link_ineligible_email(email_address, reason): + """Send an email containing a reason explaining why the User with + the given EmailAddress is ineligible to receive a login link.""" + email_enabled = import_from_settings('EMAIL_ENABLED', False) + if not email_enabled: + return + + subject = 'Ineligible for Login Link' + template_name = 'email/login_link_ineligible.txt' + context = { + 'PORTAL_NAME': settings.PORTAL_NAME, + 'reason': reason, + 'signature': import_from_settings('EMAIL_SIGNATURE', ''), + } + + sender = import_from_settings('EMAIL_SENDER') + receiver_list = [email_address.email, ] + + send_email_template(subject, template_name, context, sender, receiver_list) diff --git a/coldfront/core/user/views_/link_login_views.py b/coldfront/core/user/views_/link_login_views.py index f771e63ee..7c6343b3c 100644 --- a/coldfront/core/user/views_/link_login_views.py +++ b/coldfront/core/user/views_/link_login_views.py @@ -10,6 +10,7 @@ from coldfront.core.user.forms_.link_login_forms import RequestLoginLinkForm from coldfront.core.user.utils import send_login_link_email +from coldfront.core.user.utils import send_login_link_ineligible_email from coldfront.core.utils.common import import_from_settings @@ -23,6 +24,9 @@ class RequestLoginLinkView(FormView): form_class = RequestLoginLinkForm template_name = 'user/request_login_link.html' + class UserIneligibleException(Exception): + pass + def dispatch(self, request, *args, **kwargs): if request.user.is_authenticated: return redirect(reverse('home')) @@ -34,7 +38,13 @@ def form_valid(self, form): email = form.cleaned_data.get('email') email_address = self._validate_email_address(email) if email_address: - send_login_link_email(email_address) + try: + self._validate_user_eligible(email_address.user) + except self.UserIneligibleException as e: + reason = str(e) + send_login_link_ineligible_email(email_address, reason) + else: + send_login_link_email(email_address) self._send_success_message() return super().form_valid(form) @@ -70,6 +80,16 @@ def _validate_email_address(self, email): messages.error(self.request, message) return email_address + def _validate_user_eligible(self, user): + """Return None if the given User is eligible to log in using + this method. Otherwise, raise an exception with a user-facing + message explaining why the user is ineligible.""" + # Staff users and superusers + if user.is_staff or user.is_superuser: + raise self.UserIneligibleException( + 'For security reasons, portal staff are disallowed from ' + 'logging in using a link.') + class LinkLoginView(LoginView): """A subclass of Django Sesame's login view, with custom logic.""" diff --git a/coldfront/templates/email/login_link_ineligible.txt b/coldfront/templates/email/login_link_ineligible.txt new file mode 100644 index 000000000..7908590eb --- /dev/null +++ b/coldfront/templates/email/login_link_ineligible.txt @@ -0,0 +1,12 @@ +Dear {{ PORTAL_NAME }} user, + +You requested a link to log in to the portal. + +You are ineligible to receive a link for the following reason: + +{{ reason }} + +Please contact a system administrator if you have any questions. + +Thank you, +{{ signature }} \ No newline at end of file From 75675741f50630edb554e1690bea1c69023e3c5b Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 22 Mar 2023 10:40:12 -0700 Subject: [PATCH 15/72] Configure link-login settings in Ansible; raise error if flag combinations are invalid --- bootstrap/ansible/main.copyme | 7 ++++--- bootstrap/ansible/settings_template.tmpl | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/bootstrap/ansible/main.copyme b/bootstrap/ansible/main.copyme index cd0c670e8..af16b66a3 100644 --- a/bootstrap/ansible/main.copyme +++ b/bootstrap/ansible/main.copyme @@ -81,9 +81,10 @@ cilogon_app_client_id: "" cilogon_app_secret: "" # Django Flags settings. -# TODO: For LRC, disable basic auth. and enable SSO. -flag_basic_auth_enabled: True -flag_sso_enabled: False +# TODO: For LRC, disable link login. +flag_basic_auth_enabled: False +flag_sso_enabled: True +flag_link_login_enabled: True # TODO: For LRC, disable BRC and enable LRC. flag_brc_enabled: True flag_lrc_enabled: False diff --git a/bootstrap/ansible/settings_template.tmpl b/bootstrap/ansible/settings_template.tmpl index 21b2ddfb1..4f1557231 100644 --- a/bootstrap/ansible/settings_template.tmpl +++ b/bootstrap/ansible/settings_template.tmpl @@ -162,12 +162,28 @@ FLAGS = { ], 'BASIC_AUTH_ENABLED': [{'condition': 'boolean', 'value': {{ flag_basic_auth_enabled }}}], 'BRC_ONLY': [{'condition': 'boolean', 'value': {{ flag_brc_enabled }}}], + 'LINK_LOGIN_ENABLED': [{'condition': 'boolean', 'value': {{ flag_link_login_enabled }}}], 'LRC_ONLY': [{'condition': 'boolean', 'value': {{ flag_lrc_enabled }}}], 'SECURE_DIRS_REQUESTABLE': [{'condition': 'boolean', 'value': {{ flag_brc_enabled }}}], 'SERVICE_UNITS_PURCHASABLE': [{'condition': 'boolean', 'value': {{ flag_brc_enabled }}}], 'SSO_ENABLED': [{'condition': 'boolean', 'value': {{ flag_sso_enabled }}}], } +# Enforce that boolean flags are consistent with each other. +from django.core.exceptions import ImproperlyConfigured +if not (FLAGS['BRC_ONLY'][0]['value'] ^ FLAGS['LRC_ONLY'][0]['value']): + raise ImproperlyConfigured( + 'Exactly one of BRC_ONLY, LRC_ONLY should be enabled.') +if not ( + FLAGS['BASIC_AUTH_ENABLED'][0]['value'] ^ + FLAGS['SSO_ENABLED'][0]['value']): + raise ImproperlyConfigured( + 'Exactly one of BASIC_AUTH_ENABLED, SSO_ENABLED should be enabled.') +if not FLAGS['SSO_ENABLED'][0]['value'] and FLAGS['LINK_LOGIN_ENABLED']: + raise ImproperlyConfigured( + 'LINK_LOGIN_ENABLED should only be enabled when SSO_ENABLED is ' + 'enabled.') + #------------------------------------------------------------------------------ # django-q settings #------------------------------------------------------------------------------ From 416c982a63bd47f1f53391a5c602e33521380c75 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Wed, 22 Mar 2023 11:19:07 -0700 Subject: [PATCH 16/72] Do not send login link emails to unverified addresses --- coldfront/core/user/views_/link_login_views.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/coldfront/core/user/views_/link_login_views.py b/coldfront/core/user/views_/link_login_views.py index 7c6343b3c..c070ab823 100644 --- a/coldfront/core/user/views_/link_login_views.py +++ b/coldfront/core/user/views_/link_login_views.py @@ -64,13 +64,12 @@ def _send_success_message(self): def _validate_email_address(self, email): """Return an EmailAddress object corresponding to the given - address (str) if one exists. Otherwise, return None. Write user - and log messages as needed.""" - email_address = None + address (str) if one exists and is verified. Otherwise, return + None. Write user and log messages as needed.""" try: email_address = EmailAddress.objects.get(email=email) except EmailAddress.DoesNotExist: - pass + return None except EmailAddress.MultipleObjectsReturned: logger.error( f'Unexpectedly found multiple EmailAddresses for email ' @@ -78,7 +77,11 @@ def _validate_email_address(self, email): message = ( 'Unexpected server error. Please contact an administrator.') messages.error(self.request, message) - return email_address + return None + else: + if not email_address.verified: + return None + return email_address def _validate_user_eligible(self, user): """Return None if the given User is eligible to log in using From 4cb03a66f8a217cb2928c09a50141a614163d325 Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Thu, 23 Mar 2023 16:06:16 -0700 Subject: [PATCH 17/72] refactor sorting and initial ordering in request hub view --- ...on_cluster_account_request_list_table.html | 24 +++----- ...re_dir_manage_user_request_list_table.html | 6 +- .../secure_dir_request_list_table.html | 12 ++-- .../request_list_table.html | 28 ++------- .../project_join_request_list_table.html | 3 +- .../project_renewal_request_list_table.html | 35 ++--------- .../savio/project_request_list_table.html | 15 ++--- .../vector/project_request_list_table.html | 15 ++--- .../core/user/views_/request_hub_views.py | 58 ++++++++++--------- 9 files changed, 66 insertions(+), 130 deletions(-) diff --git a/coldfront/core/allocation/templates/allocation/allocation_cluster_account_request_list_table.html b/coldfront/core/allocation/templates/allocation/allocation_cluster_account_request_list_table.html index de0017a7d..e8c315971 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_cluster_account_request_list_table.html +++ b/coldfront/core/allocation/templates/allocation/allocation_cluster_account_request_list_table.html @@ -4,34 +4,28 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} - {% if request_filter == 'completed' %} - Completion Time - - - {% else %} + {% if request_filter == 'pending' or adj == 'pending' %} Request Time - - + {% include 'common/table_sorter.html' with table_sorter_field='request_time' %} + {% else %} + Completion Time + {% include 'common/table_sorter.html' with table_sorter_field='completion_time' %} {% endif %} User Email - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__user__email' %} Cluster Username - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__user__username' %} Project - - + {% include 'common/table_sorter.html' with table_sorter_field='project_user__project__name' %} Allocation diff --git a/coldfront/core/allocation/templates/secure_dir/secure_dir_manage_user_request_list_table.html b/coldfront/core/allocation/templates/secure_dir/secure_dir_manage_user_request_list_table.html index 366075a51..3e20d1508 100644 --- a/coldfront/core/allocation/templates/secure_dir/secure_dir_manage_user_request_list_table.html +++ b/coldfront/core/allocation/templates/secure_dir/secure_dir_manage_user_request_list_table.html @@ -3,8 +3,7 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} {% if request_filter == 'pending' or adj == 'pending' %} @@ -12,8 +11,7 @@ {% else %} Date Completed {% endif %} - - + {% include 'common/table_sorter.html' with table_sorter_field='created' %} User diff --git a/coldfront/core/allocation/templates/secure_dir/secure_dir_request/secure_dir_request_list_table.html b/coldfront/core/allocation/templates/secure_dir/secure_dir_request/secure_dir_request_list_table.html index 44d7f543a..d6c694152 100644 --- a/coldfront/core/allocation/templates/secure_dir/secure_dir_request/secure_dir_request_list_table.html +++ b/coldfront/core/allocation/templates/secure_dir/secure_dir_request/secure_dir_request_list_table.html @@ -3,23 +3,19 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} Requested - - + {% include 'common/table_sorter.html' with table_sorter_field='request_time' %} Requester - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__email' %} Project - - + {% include 'common/table_sorter.html' with table_sorter_field='project' %} Status diff --git a/coldfront/core/project/templates/project/project_allocation_addition/request_list_table.html b/coldfront/core/project/templates/project/project_allocation_addition/request_list_table.html index 3a1fae7d9..b6538d302 100644 --- a/coldfront/core/project/templates/project/project_allocation_addition/request_list_table.html +++ b/coldfront/core/project/templates/project/project_allocation_addition/request_list_table.html @@ -3,39 +3,19 @@ # - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} Date Requested/
    Last Modified - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='modified' %} Requester Email - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__email' %} Project - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='project' %} Number of Service Units Status diff --git a/coldfront/core/project/templates/project/project_join_request_list_table.html b/coldfront/core/project/templates/project/project_join_request_list_table.html index 20fcb94e3..e5e806bc2 100644 --- a/coldfront/core/project/templates/project/project_join_request_list_table.html +++ b/coldfront/core/project/templates/project/project_join_request_list_table.html @@ -15,8 +15,7 @@ Date Requested - - + {% include 'common/table_sorter.html' with table_sorter_field='created' %} Reason diff --git a/coldfront/core/project/templates/project/project_renewal/project_renewal_request_list_table.html b/coldfront/core/project/templates/project/project_renewal/project_renewal_request_list_table.html index a13811d24..f31a1d9fc 100644 --- a/coldfront/core/project/templates/project/project_renewal/project_renewal_request_list_table.html +++ b/coldfront/core/project/templates/project/project_renewal/project_renewal_request_list_table.html @@ -3,48 +3,23 @@ # - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} Requested - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='request_time' %} Requester - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__email' %} Project - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='project' %} PI - - - - - - + {% include 'common/table_sorter.html' with table_sorter_field='pi' %} Status diff --git a/coldfront/core/project/templates/project/project_request/savio/project_request_list_table.html b/coldfront/core/project/templates/project/project_request/savio/project_request_list_table.html index 3291afe1b..bc87e515f 100644 --- a/coldfront/core/project/templates/project/project_request/savio/project_request_list_table.html +++ b/coldfront/core/project/templates/project/project_request/savio/project_request_list_table.html @@ -3,28 +3,23 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} Requested - - + {% include 'common/table_sorter.html' with table_sorter_field='request_time' %} Requester - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__email' %} Project - - + {% include 'common/table_sorter.html' with table_sorter_field='project' %} PI - - + {% include 'common/table_sorter.html' with table_sorter_field='pi' %} Status diff --git a/coldfront/core/project/templates/project/project_request/vector/project_request_list_table.html b/coldfront/core/project/templates/project/project_request/vector/project_request_list_table.html index 7f9ce2da7..0525a5ef7 100644 --- a/coldfront/core/project/templates/project/project_request/vector/project_request_list_table.html +++ b/coldfront/core/project/templates/project/project_request/vector/project_request_list_table.html @@ -3,28 +3,23 @@ # - - + {% include 'common/table_sorter.html' with table_sorter_field='id' %} Date Requested/
    Last Modified - - + {% include 'common/table_sorter.html' with table_sorter_field='modified' %} Requester Email - - + {% include 'common/table_sorter.html' with table_sorter_field='requester__email' %} Project - - + {% include 'common/table_sorter.html' with table_sorter_field='project' %} PI - - + {% include 'common/table_sorter.html' with table_sorter_field='pi' %} Status diff --git a/coldfront/core/user/views_/request_hub_views.py b/coldfront/core/user/views_/request_hub_views.py index 5f36368fd..6d8e14167 100644 --- a/coldfront/core/user/views_/request_hub_views.py +++ b/coldfront/core/user/views_/request_hub_views.py @@ -27,6 +27,7 @@ class RequestListItem: Object to keep track of all variables used in for each request type in the request hub """ + def __init__(self): num = None title = None @@ -89,12 +90,12 @@ def get_cluster_account_request(self): cluster_account_list_complete = \ ClusterAccessRequest.objects.filter( status__name__in=['Denied', 'Complete'], **kwargs).order_by( - 'modified') + '-modified') cluster_account_list_pending = \ ClusterAccessRequest.objects.filter( status__name__in=['Pending - Add', 'Processing'], **kwargs).order_by( - 'modified') + '-modified') cluster_request_object.num = self.paginators cluster_request_object.pending_queryset = \ @@ -130,12 +131,12 @@ def get_project_removal_request(self): removal_request_pending = \ ProjectUserRemovalRequest.objects.filter( status__name__in=['Pending', 'Processing'], *args).order_by( - 'modified') + '-modified') removal_request_complete = \ ProjectUserRemovalRequest.objects.filter( status__name='Complete', *args).order_by( - 'modified') + '-modified') removal_request_object.num = self.paginators removal_request_object.pending_queryset = \ @@ -174,7 +175,7 @@ def get_savio_project_request(self): annotate_queryset_with_allocation_period_not_started_bool( SavioProjectAllocationRequest.objects.filter( status__name__in=pending_status_names, *args - ).order_by('request_time')) + ).order_by('-request_time')) complete_status_names = [ 'Approved - Complete', 'Approved - Scheduled', 'Denied'] @@ -182,7 +183,7 @@ def get_savio_project_request(self): annotate_queryset_with_allocation_period_not_started_bool( SavioProjectAllocationRequest.objects.filter( status__name__in=complete_status_names, *args - ).order_by('request_time')) + ).order_by('-request_time')) savio_proj_request_object.num = self.paginators savio_proj_request_object.pending_queryset = \ @@ -219,14 +220,12 @@ def get_vector_project_request(self): project_request_pending = \ VectorProjectAllocationRequest.objects.filter( status__name__in=['Under Review', 'Approved - Processing'], - *args).order_by( - 'modified') + *args).order_by('-modified') project_request_complete = \ VectorProjectAllocationRequest.objects.filter( status__name__in=['Approved - Complete', 'Denied'], - *args).order_by( - 'modified') + *args).order_by('-modified') vector_proj_request_object.num = self.paginators vector_proj_request_object.pending_queryset = \ @@ -263,12 +262,12 @@ def get_project_join_request(self): project_join_request_pending = \ ProjectUserJoinRequest.objects.filter( project_user__status__name='Pending - Add', - *args).order_by('modified') + *args).order_by('-modified') project_join_request_complete = \ ProjectUserJoinRequest.objects.filter( project_user__status__name__in=['Active', 'Denied'], - *args).order_by('modified') + *args).order_by('-modified') proj_join_request_object.num = self.paginators proj_join_request_object.pending_queryset = \ @@ -308,14 +307,14 @@ def get_project_renewal_request(self): annotate_queryset_with_allocation_period_not_started_bool( AllocationRenewalRequest.objects.filter( status__name__in=pending_status_names, *args - ).order_by('request_time')) + ).order_by('-request_time')) complete_status_names = ['Approved', 'Complete', 'Denied'] project_renewal_request_complete = \ annotate_queryset_with_allocation_period_not_started_bool( AllocationRenewalRequest.objects.filter( status__name__in=complete_status_names, *args - ).order_by('request_time')) + ).order_by('-request_time')) proj_renewal_request_object.num = self.paginators proj_renewal_request_object.pending_queryset = \ @@ -347,10 +346,10 @@ def get_su_purchase_request(self): user = self.request.user su_purchase_request_pending = AllocationAdditionRequest.objects.filter( - status__name__in=['Under Review']).order_by('modified') + status__name__in=['Under Review']).order_by('-modified') su_purchase_request_complete = AllocationAdditionRequest.objects.filter( - status__name__in=['Complete', 'Denied']).order_by('modified') + status__name__in=['Complete', 'Denied']).order_by('-modified') if not self.show_all_requests: request_ids = [ @@ -395,10 +394,10 @@ def get_secure_dir_join_request(self): user = self.request.user secure_dir_join_pending = SecureDirAddUserRequest.objects.filter( - status__name__in=['Pending', 'Processing']).order_by('modified') + status__name__in=['Pending', 'Processing']).order_by('-modified') secure_dir_join_complete = SecureDirAddUserRequest.objects.filter( - status__name__in=['Complete', 'Denied']).order_by('modified') + status__name__in=['Complete', 'Denied']).order_by('-modified') if not self.show_all_requests: # limit secure_dir_requests to objects user is a PI of or user has @@ -411,7 +410,8 @@ def get_secure_dir_join_request(self): ).exists()] pi_cond = Q(pk__in=request_pks) - secure_dir_join_pending = secure_dir_join_pending.filter(user_cond | pi_cond) + secure_dir_join_pending = secure_dir_join_pending.filter( + user_cond | pi_cond) request_pks = [request.pk for request in secure_dir_join_complete if request.allocation.project.projectuser_set.filter( @@ -421,7 +421,8 @@ def get_secure_dir_join_request(self): ).exists()] pi_cond = Q(pk__in=request_pks) - secure_dir_join_complete = secure_dir_join_complete.filter(user_cond | pi_cond) + secure_dir_join_complete = secure_dir_join_complete.filter( + user_cond | pi_cond) secure_dir_join_request_object.num = self.paginators secure_dir_join_request_object.pending_queryset = \ @@ -458,10 +459,10 @@ def get_secure_dir_remove_request(self): user = self.request.user secure_dir_remove_pending = SecureDirRemoveUserRequest.objects.filter( - status__name__in=['Pending', 'Processing']).order_by('modified') + status__name__in=['Pending', 'Processing']).order_by('-modified') secure_dir_remove_complete = SecureDirRemoveUserRequest.objects.filter( - status__name__in=['Complete', 'Denied']).order_by('modified') + status__name__in=['Complete', 'Denied']).order_by('-modified') if not self.show_all_requests: # limit secure_dir_requests to objects user is a PI of or user has @@ -474,7 +475,8 @@ def get_secure_dir_remove_request(self): ).exists()] pi_cond = Q(pk__in=request_pks) - secure_dir_remove_pending = secure_dir_remove_pending.filter(user_cond | pi_cond) + secure_dir_remove_pending = secure_dir_remove_pending.filter( + user_cond | pi_cond) request_pks = [request.pk for request in secure_dir_remove_complete if request.allocation.project.projectuser_set.filter( @@ -484,7 +486,8 @@ def get_secure_dir_remove_request(self): ).exists()] pi_cond = Q(pk__in=request_pks) - secure_dir_remove_complete = secure_dir_remove_complete.filter(user_cond | pi_cond) + secure_dir_remove_complete = secure_dir_remove_complete.filter( + user_cond | pi_cond) secure_dir_remove_request_object.num = self.paginators secure_dir_remove_request_object.pending_queryset = \ @@ -522,10 +525,10 @@ def get_secure_dir_request(self): user = self.request.user secure_dir_pending = SecureDirRequest.objects.filter( - status__name__in=['Under Review', 'Approved - Processing']).order_by('modified') + status__name__in=['Under Review', 'Approved - Processing']).order_by('-modified') secure_dir_complete = SecureDirRequest.objects.filter( - status__name__in=['Approved - Complete', 'Denied']).order_by('modified') + status__name__in=['Approved - Complete', 'Denied']).order_by('-modified') if not self.show_all_requests: # limit secure_dir_requests to objects user is a PI of or user has @@ -548,7 +551,8 @@ def get_secure_dir_request(self): ).exists()] pi_cond = Q(pk__in=request_pks) - secure_dir_complete = secure_dir_complete.filter(user_cond | pi_cond) + secure_dir_complete = secure_dir_complete.filter( + user_cond | pi_cond) secure_dir_request_object.num = self.paginators secure_dir_request_object.pending_queryset = \ From 4acb6c79a69785c4fa47b4a1261e9afe594715c0 Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Thu, 23 Mar 2023 16:36:18 -0700 Subject: [PATCH 18/72] update initial order for individual requests list views --- coldfront/core/allocation/views_/secure_dir_views.py | 4 ++-- .../core/project/views_/addition_views/approval_views.py | 2 +- coldfront/core/project/views_/join_views/approval_views.py | 4 ++-- .../core/project/views_/new_project_views/approval_views.py | 4 ++-- coldfront/core/project/views_/removal_views.py | 2 +- coldfront/core/project/views_/renewal_views/approval_views.py | 3 ++- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/coldfront/core/allocation/views_/secure_dir_views.py b/coldfront/core/allocation/views_/secure_dir_views.py index f53e07374..9ee0e7cea 100644 --- a/coldfront/core/allocation/views_/secure_dir_views.py +++ b/coldfront/core/allocation/views_/secure_dir_views.py @@ -353,7 +353,7 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-modified' pending_status = self.request_status_obj.objects.filter( Q(name__icontains='Pending') | Q(name__icontains='Processing')) @@ -1091,7 +1091,7 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-modified' return SecureDirRequest.objects.order_by(order_by) diff --git a/coldfront/core/project/views_/addition_views/approval_views.py b/coldfront/core/project/views_/addition_views/approval_views.py index 8eaf4da98..8fdaaebc6 100644 --- a/coldfront/core/project/views_/addition_views/approval_views.py +++ b/coldfront/core/project/views_/addition_views/approval_views.py @@ -240,7 +240,7 @@ def get_order_by(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-modified' return order_by def test_func(self): diff --git a/coldfront/core/project/views_/join_views/approval_views.py b/coldfront/core/project/views_/join_views/approval_views.py index 0e1f25a96..89bd92935 100644 --- a/coldfront/core/project/views_/join_views/approval_views.py +++ b/coldfront/core/project/views_/join_views/approval_views.py @@ -244,9 +244,9 @@ def get_queryset(self): direction = '' else: direction = '-' - order_by = direction + 'created' + order_by = direction + order_by else: - order_by = '-created' + order_by = '-modified' project_join_requests = \ ProjectUserJoinRequest.objects.filter( diff --git a/coldfront/core/project/views_/new_project_views/approval_views.py b/coldfront/core/project/views_/new_project_views/approval_views.py index 9d42339bb..0b5d1b5c0 100644 --- a/coldfront/core/project/views_/new_project_views/approval_views.py +++ b/coldfront/core/project/views_/new_project_views/approval_views.py @@ -69,7 +69,7 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-request_time' return annotate_queryset_with_allocation_period_not_started_bool( SavioProjectAllocationRequest.objects.order_by(order_by)) @@ -910,7 +910,7 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-modified' return VectorProjectAllocationRequest.objects.order_by(order_by) def get_context_data(self, **kwargs): diff --git a/coldfront/core/project/views_/removal_views.py b/coldfront/core/project/views_/removal_views.py index ffba9b603..62bfcb307 100644 --- a/coldfront/core/project/views_/removal_views.py +++ b/coldfront/core/project/views_/removal_views.py @@ -217,7 +217,7 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-modified' project_removal_status_complete, _ = \ ProjectUserRemovalRequestStatusChoice.objects.get_or_create( diff --git a/coldfront/core/project/views_/renewal_views/approval_views.py b/coldfront/core/project/views_/renewal_views/approval_views.py index a331f6a19..447361b80 100644 --- a/coldfront/core/project/views_/renewal_views/approval_views.py +++ b/coldfront/core/project/views_/renewal_views/approval_views.py @@ -55,7 +55,8 @@ def get_queryset(self): direction = '-' order_by = direction + order_by else: - order_by = 'id' + order_by = '-request_time' + return annotate_queryset_with_allocation_period_not_started_bool( AllocationRenewalRequest.objects.order_by(order_by)) From 2e944fbf4d76a644f727f690fc21bcf4aa0c5091 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Tue, 28 Mar 2023 00:44:42 -0500 Subject: [PATCH 19/72] initial docker-compose --- Dockerfile | 66 +++++++++++++++--------- bootstrap/ansible/main.copyme | 2 + bootstrap/ansible/settings_template.tmpl | 6 +-- docker-compose.yml | 60 +++++++++++++++++++++ 4 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile index b56ea76b9..041b6079c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,50 @@ -FROM centos:8 +FROM centos/python-38-centos7 LABEL description="coldfront" + # install dependencies -RUN yum -y install epel-release -RUN yum -y update -RUN yum -y install python36 python36-devel git memcached redis +# RUN yum -y install epel-release +# RUN yum -y update +# RUN yum -y install python36 python36-devel git memcached redis +USER root WORKDIR /root # install coldfront -RUN mkdir /opt/coldfront_app - -WORKDIR /opt/coldfront_app - -RUN cd /opt/coldfront_app -RUN git clone https://github.com/ubccr/coldfront.git -RUN python3.6 -mvenv venv -RUN source venv/bin/activate - -WORKDIR /opt/coldfront_app/coldfront - -RUN cd /opt/coldfront_app/coldfront -RUN pip3 install wheel -RUN pip3 install -r requirements.txt -RUN cp coldfront/config/local_settings.py.sample coldfront/config/local_settings.py -RUN cp coldfront/config/local_strings.py.sample coldfront/config/local_strings.py - -RUN python3 ./manage.py initial_setup -RUN python3 ./manage.py load_test_data - -EXPOSE 8000 +RUN mkdir -p /vagrant/coldfront_app +WORKDIR /vagrant/coldfront_app/ +RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git +WORKDIR ./coldfront +COPY main.yml ./main.yml + +RUN pip3 install wheel jinja2-cli pyyaml \ + && pip3 install -r requirements.txt + +RUN cp coldfront/config/local_settings.py.sample \ + coldfront/config/local_settings.py \ + && cp coldfront/config/local_strings.py.sample \ + coldfront/config/local_strings.py \ + && python -c \ +"from jinja2 import Template, Environment, FileSystemLoader; \ +import yaml; \ +env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ +env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ +options = yaml.safe_load(open('main.yml').read()); \ +options.update({'redis_host': 'redis', 'db_host': 'db', \ + 'debug_toolbar_ips':['host.docker.internal']}); \ +print(env.get_template('settings_template.tmpl').render(options))" \ + > coldfront/config/dev_settings.py + +RUN mkdir -p /var/log/user_portals/cf_mybrc \ + && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod 775 /var/log/user_portals/cf_mybrc \ + && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod +x ./manage.py + + +CMD ./manage.py initial_setup \ + && ./manage.py runserver + +EXPOSE 8080 STOPSIGNAL SIGINT diff --git a/bootstrap/ansible/main.copyme b/bootstrap/ansible/main.copyme index cd0c670e8..a3ac645c1 100644 --- a/bootstrap/ansible/main.copyme +++ b/bootstrap/ansible/main.copyme @@ -21,6 +21,7 @@ djangoprojname: coldfront # The name of the PostgreSQL database. # TODO: For LRC, set this to 'cf_lrc_db'. db_name: cf_brc_db +db_host: localhost # The credentials for the database admin user. # TODO: Replace the username and password. @@ -30,6 +31,7 @@ db_admin_passwd: '' # The password for Redis. # TODO: Replace the password. redis_passwd: '' +redis_host: localhost # Log paths. # TODO: For LRC, use the substring 'cf_mylrc'. diff --git a/bootstrap/ansible/settings_template.tmpl b/bootstrap/ansible/settings_template.tmpl index 21b2ddfb1..818c6abfe 100644 --- a/bootstrap/ansible/settings_template.tmpl +++ b/bootstrap/ansible/settings_template.tmpl @@ -46,7 +46,7 @@ DATABASES = { 'NAME': '{{ db_name }}', 'USER': '{{ db_admin_user }}', 'PASSWORD': '{{ db_admin_passwd }}', - 'HOST': 'localhost', + 'HOST': '{{ db_host }}', 'PORT': '5432', }, } @@ -135,7 +135,7 @@ CONSTANCE_CONFIG = { 'LAUNCH_DATE': (date(1970, 1, 1), 'The date the portal was launched.'), } CONSTANCE_REDIS_CONNECTION = { - 'host': '127.0.0.1', + 'host': '{{ redis_host }}', 'port': 6379, 'db': 0, 'password': '{{ redis_passwd }}', @@ -174,7 +174,7 @@ FLAGS = { Q_CLUSTER = { 'redis': { - 'host': '127.0.0.1', + 'host': '{{ redis_host }}', 'port': 6379, 'db': 0, 'password': '{{ redis_passwd }}', diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..e6e4af424 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,60 @@ +# Decompoe the stack into microservices running in Docker, orchestrated with Docker Compose. +# Use cases: + +# People with MacBooks with M1 chips aren’t able to use VirtualBox right now, so we need an alternative for them. +# The group is bringing up an internal Kubernetes cluster. It’d be nice to have multiple staging instances that we can easily bring up so that we can stage multiple branches at the same time. +# This should be more lightweight/faster than a VM. +# The deliverable would be a set of Docker images (Postgres, Redis, one to run the web service, etc.) and a Docker Compose file to run them. +# I’m not exactly sure how Ansible is going to fit into this picture. Maybe Ansible just creates configuration files that get injected into containers. Let me know if you have thoughts on / experience with this.# Docker Compose + +services: + db: + image: postgres:15 + ports: + - 5432:5432 + volumes: + - db_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: cf_brc_db + POSTGRES_USER: admin + POSTGRES_PASSWORD: root + healthcheck: + test: ["CMD", "pg_isready", "-U", "admin"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + redis: + image: redis:7 + ports: + - 6379:6379 + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + coldfront: + image: coldfront:latest + build: + context: . + dockerfile: Dockerfile + ports: + - 8880:8000 + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + restart: unless-stopped + # network_mode: host + +volumes: + db_data: + external: false + redis_data: + external: false From 1c50483469dddf2904f75f6d1585bf8868859b6e Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Tue, 28 Mar 2023 00:54:57 -0500 Subject: [PATCH 20/72] cleaned up dockerfile --- Dockerfile | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 041b6079c..806617d67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,16 +2,9 @@ FROM centos/python-38-centos7 LABEL description="coldfront" - -# install dependencies -# RUN yum -y install epel-release -# RUN yum -y update -# RUN yum -y install python36 python36-devel git memcached redis - USER root WORKDIR /root -# install coldfront RUN mkdir -p /vagrant/coldfront_app WORKDIR /vagrant/coldfront_app/ RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git @@ -42,7 +35,6 @@ RUN mkdir -p /var/log/user_portals/cf_mybrc \ && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ && chmod +x ./manage.py - CMD ./manage.py initial_setup \ && ./manage.py runserver From 015c42dabad3d806ef521309502b5d3891140976 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Tue, 28 Mar 2023 16:54:36 -0500 Subject: [PATCH 21/72] more changes for docker --- Dockerfile | 64 ++++++++----------- coldfront/config/local_settings.py.sample | 5 ++ docker/Dockerfile | 43 +++++++++++++ .../docker-compose.yml | 9 +-- 4 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 docker/Dockerfile rename docker-compose.yml => docker/docker-compose.yml (91%) diff --git a/Dockerfile b/Dockerfile index 806617d67..b56ea76b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,42 +1,34 @@ -FROM centos/python-38-centos7 +FROM centos:8 LABEL description="coldfront" -USER root +# install dependencies +RUN yum -y install epel-release +RUN yum -y update +RUN yum -y install python36 python36-devel git memcached redis + WORKDIR /root -RUN mkdir -p /vagrant/coldfront_app -WORKDIR /vagrant/coldfront_app/ -RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git -WORKDIR ./coldfront -COPY main.yml ./main.yml - -RUN pip3 install wheel jinja2-cli pyyaml \ - && pip3 install -r requirements.txt - -RUN cp coldfront/config/local_settings.py.sample \ - coldfront/config/local_settings.py \ - && cp coldfront/config/local_strings.py.sample \ - coldfront/config/local_strings.py \ - && python -c \ -"from jinja2 import Template, Environment, FileSystemLoader; \ -import yaml; \ -env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ -env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ -options = yaml.safe_load(open('main.yml').read()); \ -options.update({'redis_host': 'redis', 'db_host': 'db', \ - 'debug_toolbar_ips':['host.docker.internal']}); \ -print(env.get_template('settings_template.tmpl').render(options))" \ - > coldfront/config/dev_settings.py - -RUN mkdir -p /var/log/user_portals/cf_mybrc \ - && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ - && chmod 775 /var/log/user_portals/cf_mybrc \ - && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ - && chmod +x ./manage.py - -CMD ./manage.py initial_setup \ - && ./manage.py runserver - -EXPOSE 8080 +# install coldfront +RUN mkdir /opt/coldfront_app + +WORKDIR /opt/coldfront_app + +RUN cd /opt/coldfront_app +RUN git clone https://github.com/ubccr/coldfront.git +RUN python3.6 -mvenv venv +RUN source venv/bin/activate + +WORKDIR /opt/coldfront_app/coldfront + +RUN cd /opt/coldfront_app/coldfront +RUN pip3 install wheel +RUN pip3 install -r requirements.txt +RUN cp coldfront/config/local_settings.py.sample coldfront/config/local_settings.py +RUN cp coldfront/config/local_strings.py.sample coldfront/config/local_strings.py + +RUN python3 ./manage.py initial_setup +RUN python3 ./manage.py load_test_data + +EXPOSE 8000 STOPSIGNAL SIGINT diff --git a/coldfront/config/local_settings.py.sample b/coldfront/config/local_settings.py.sample index d5164bebb..d4af5a466 100644 --- a/coldfront/config/local_settings.py.sample +++ b/coldfront/config/local_settings.py.sample @@ -385,6 +385,11 @@ INTERNAL_IPS = [ '127.0.0.1' ] +if DEBUG: + import socket + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + INTERNAL_IPS = [ip[: ip.rfind('.')] + '.1' for ip in ips] + ['10.0.2.2'] + # The number of hours for which a newly created authentication token will be # valid. TOKEN_EXPIRATION_HOURS = 24 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..45019a954 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,43 @@ +FROM centos/python-38-centos7 + +LABEL description="coldfront" + +USER root +WORKDIR /root + +RUN mkdir -p /vagrant/coldfront_app +WORKDIR /vagrant/coldfront_app/ +RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git +WORKDIR ./coldfront +COPY main.yml ./main.yml + +RUN pip3 install wheel jinja2-cli pyyaml \ + && pip3 install -r requirements.txt + +RUN cp coldfront/config/local_settings.py.sample \ + coldfront/config/local_settings.py \ + && cp coldfront/config/local_strings.py.sample \ + coldfront/config/local_strings.py \ + && python -c \ +"from jinja2 import Template, Environment, FileSystemLoader; \ +import yaml; \ +env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ +env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ +options = yaml.safe_load(open('main.yml').read()); \ +options.update({'redis_host': 'redis', 'db_host': 'db', \ + 'debug_toolbar_ips': \ + [type(str('c'), (), {'__contains__': lambda *a: True})()]}); \ +print(env.get_template('settings_template.tmpl').render(options))" \ + > coldfront/config/dev_settings.py + +RUN mkdir -p /var/log/user_portals/cf_mybrc \ + && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod 775 /var/log/user_portals/cf_mybrc \ + && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod +x ./manage.py + +CMD ./manage.py initial_setup \ + && ./manage.py runserver 0.0.0.0:80 + +EXPOSE 80 +STOPSIGNAL SIGINT diff --git a/docker-compose.yml b/docker/docker-compose.yml similarity index 91% rename from docker-compose.yml rename to docker/docker-compose.yml index e6e4af424..b1afefd80 100644 --- a/docker-compose.yml +++ b/docker/docker-compose.yml @@ -19,7 +19,7 @@ services: POSTGRES_USER: admin POSTGRES_PASSWORD: root healthcheck: - test: ["CMD", "pg_isready", "-U", "admin"] + test: ["CMD", "pg_isready", "-U", "admin", "-d", "cf_brc_db"] interval: 10s timeout: 5s retries: 5 @@ -44,17 +44,18 @@ services: context: . dockerfile: Dockerfile ports: - - 8880:8000 + - 8880:80 + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: db: condition: service_healthy redis: condition: service_healthy restart: unless-stopped - # network_mode: host volumes: db_data: external: false redis_data: - external: false + external: false \ No newline at end of file From 6bcc066fb295aa11412dbe7dc623641271eefc71 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Tue, 28 Mar 2023 17:04:06 -0500 Subject: [PATCH 22/72] removed redundant setting --- docker/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 45019a954..660ef0a25 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,9 +24,7 @@ import yaml; \ env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ options = yaml.safe_load(open('main.yml').read()); \ -options.update({'redis_host': 'redis', 'db_host': 'db', \ - 'debug_toolbar_ips': \ - [type(str('c'), (), {'__contains__': lambda *a: True})()]}); \ +options.update({'redis_host': 'redis', 'db_host': 'db'}); \ print(env.get_template('settings_template.tmpl').render(options))" \ > coldfront/config/dev_settings.py From 6f50b6e76d0c65fa2641f4ac70f804fcaae510e0 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Tue, 28 Mar 2023 17:06:04 -0500 Subject: [PATCH 23/72] newline at end of file --- docker/docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b1afefd80..817d823a6 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -58,4 +58,5 @@ volumes: db_data: external: false redis_data: - external: false \ No newline at end of file + external: false + From 48148acfe8a0f12b2c4f3f28738c153853f296f6 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Wed, 29 Mar 2023 17:03:22 -0500 Subject: [PATCH 24/72] finished pt. 1 --- Dockerfile | 72 +++++++++++-------- .../docker-compose.yml => docker-compose.yml | 2 + docker/Dockerfile | 41 ----------- 3 files changed, 45 insertions(+), 70 deletions(-) rename docker/docker-compose.yml => docker-compose.yml (96%) delete mode 100644 docker/Dockerfile diff --git a/Dockerfile b/Dockerfile index b56ea76b9..ff9355e1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,48 @@ -FROM centos:8 +FROM centos/python-38-centos7 LABEL description="coldfront" -# install dependencies -RUN yum -y install epel-release -RUN yum -y update -RUN yum -y install python36 python36-devel git memcached redis - +USER root WORKDIR /root - -# install coldfront -RUN mkdir /opt/coldfront_app - -WORKDIR /opt/coldfront_app - -RUN cd /opt/coldfront_app -RUN git clone https://github.com/ubccr/coldfront.git -RUN python3.6 -mvenv venv -RUN source venv/bin/activate - -WORKDIR /opt/coldfront_app/coldfront - -RUN cd /opt/coldfront_app/coldfront -RUN pip3 install wheel -RUN pip3 install -r requirements.txt -RUN cp coldfront/config/local_settings.py.sample coldfront/config/local_settings.py -RUN cp coldfront/config/local_strings.py.sample coldfront/config/local_strings.py - -RUN python3 ./manage.py initial_setup -RUN python3 ./manage.py load_test_data - -EXPOSE 8000 +COPY requirements.txt ./ +RUN pip install -r requirements.txt \ + && pip install jinja2 pyyaml && rm requirements.txt + +RUN mkdir -p /vagrant/coldfront_app +WORKDIR /vagrant/coldfront_app/ +RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git +WORKDIR ./coldfront +COPY main.yml ./main.yml + +RUN mkdir -p /var/log/user_portals/cf_mybrc \ + && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod 775 /var/log/user_portals/cf_mybrc \ + && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ + && chmod +x ./manage.py + +RUN cp coldfront/config/local_settings.py.sample \ + coldfront/config/local_settings.py \ + && cp coldfront/config/local_strings.py.sample \ + coldfront/config/local_strings.py \ + && python -c \ +"from jinja2 import Template, Environment, FileSystemLoader; \ +import yaml; \ +env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ +env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ +options = yaml.safe_load(open('main.yml').read()); \ +options.update({'redis_host': 'redis', 'db_host': 'db'}); \ +print(env.get_template('settings_template.tmpl').render(options))" \ + > coldfront/config/dev_settings.py + +CMD ./manage.py initial_setup \ + && ./manage.py migrate \ + && ./manage.py add_accounting_defaults \ + && ./manage.py add_allowance_defaults \ + && ./manage.py add_directory_defaults \ + && ./manage.py create_allocation_periods \ + && ./manage.py create_staff_group \ + && ./manage.py collectstatic \ + && ./manage.py runserver 0.0.0.0:80 + +EXPOSE 80 STOPSIGNAL SIGINT diff --git a/docker/docker-compose.yml b/docker-compose.yml similarity index 96% rename from docker/docker-compose.yml rename to docker-compose.yml index 817d823a6..bf6450e72 100644 --- a/docker/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,8 @@ services: dockerfile: Dockerfile ports: - 8880:80 + volumes: + - ./coldfront:/vagrant/coldfront_app/coldfront/coldfront extra_hosts: - "host.docker.internal:host-gateway" depends_on: diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 660ef0a25..000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM centos/python-38-centos7 - -LABEL description="coldfront" - -USER root -WORKDIR /root - -RUN mkdir -p /vagrant/coldfront_app -WORKDIR /vagrant/coldfront_app/ -RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git -WORKDIR ./coldfront -COPY main.yml ./main.yml - -RUN pip3 install wheel jinja2-cli pyyaml \ - && pip3 install -r requirements.txt - -RUN cp coldfront/config/local_settings.py.sample \ - coldfront/config/local_settings.py \ - && cp coldfront/config/local_strings.py.sample \ - coldfront/config/local_strings.py \ - && python -c \ -"from jinja2 import Template, Environment, FileSystemLoader; \ -import yaml; \ -env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ -env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ -options = yaml.safe_load(open('main.yml').read()); \ -options.update({'redis_host': 'redis', 'db_host': 'db'}); \ -print(env.get_template('settings_template.tmpl').render(options))" \ - > coldfront/config/dev_settings.py - -RUN mkdir -p /var/log/user_portals/cf_mybrc \ - && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ - && chmod 775 /var/log/user_portals/cf_mybrc \ - && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ - && chmod +x ./manage.py - -CMD ./manage.py initial_setup \ - && ./manage.py runserver 0.0.0.0:80 - -EXPOSE 80 -STOPSIGNAL SIGINT From 392c0cbc9c8527f6a2d3db0fa565e52baf91b2b1 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Wed, 29 Mar 2023 17:15:08 -0500 Subject: [PATCH 25/72] using mounts instead of images --- Dockerfile | 21 ++------------------- docker-compose.yml | 2 +- gen_config.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 20 deletions(-) create mode 100644 gen_config.py diff --git a/Dockerfile b/Dockerfile index ff9355e1b..69587dc49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,11 +8,8 @@ COPY requirements.txt ./ RUN pip install -r requirements.txt \ && pip install jinja2 pyyaml && rm requirements.txt -RUN mkdir -p /vagrant/coldfront_app -WORKDIR /vagrant/coldfront_app/ -RUN git clone -b issue_521 https://github.com/ucb-rit/coldfront.git -WORKDIR ./coldfront -COPY main.yml ./main.yml +COPY . /vagrant/coldfront_app/coldfront/ +WORKDIR /vagrant/coldfront_app/coldfront/ RUN mkdir -p /var/log/user_portals/cf_mybrc \ && touch /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ @@ -20,20 +17,6 @@ RUN mkdir -p /var/log/user_portals/cf_mybrc \ && chmod 664 /var/log/user_portals/cf_mybrc/cf_mybrc_{portal,api}.log \ && chmod +x ./manage.py -RUN cp coldfront/config/local_settings.py.sample \ - coldfront/config/local_settings.py \ - && cp coldfront/config/local_strings.py.sample \ - coldfront/config/local_strings.py \ - && python -c \ -"from jinja2 import Template, Environment, FileSystemLoader; \ -import yaml; \ -env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ -env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ -options = yaml.safe_load(open('main.yml').read()); \ -options.update({'redis_host': 'redis', 'db_host': 'db'}); \ -print(env.get_template('settings_template.tmpl').render(options))" \ - > coldfront/config/dev_settings.py - CMD ./manage.py initial_setup \ && ./manage.py migrate \ && ./manage.py add_accounting_defaults \ diff --git a/docker-compose.yml b/docker-compose.yml index bf6450e72..fe6b83adc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: ports: - 8880:80 volumes: - - ./coldfront:/vagrant/coldfront_app/coldfront/coldfront + - ./:/vagrant/coldfront_app/coldfront/ extra_hosts: - "host.docker.internal:host-gateway" depends_on: diff --git a/gen_config.py b/gen_config.py new file mode 100644 index 000000000..7382379ed --- /dev/null +++ b/gen_config.py @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +cp coldfront/config/local_settings.py.sample \ + coldfront/config/local_settings.py +cp coldfront/config/local_strings.py.sample \ + coldfront/config/local_strings.py +python -c \ +"from jinja2 import Template, Environment, FileSystemLoader; \ +import yaml; \ +env = Environment(loader=FileSystemLoader('bootstrap/ansible/')); \ +env.filters['bool'] = lambda x: str(x).lower() in ['true', 'yes', 'on', '1']; \ +options = yaml.safe_load(open('main.yml').read()); \ +options.update({'redis_host': 'redis', 'db_host': 'db'}); \ +print(env.get_template('settings_template.tmpl').render(options))" \ + > coldfront/config/dev_settings.py From 7a639bac66fc7261e3bf22006d347d9a9be66726 Mon Sep 17 00:00:00 2001 From: Hamza Kundi Date: Wed, 29 Mar 2023 17:20:47 -0500 Subject: [PATCH 26/72] --noinput for collectstatic --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 69587dc49..d0a9c4185 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ CMD ./manage.py initial_setup \ && ./manage.py add_directory_defaults \ && ./manage.py create_allocation_periods \ && ./manage.py create_staff_group \ - && ./manage.py collectstatic \ + && ./manage.py collectstatic --noinput \ && ./manage.py runserver 0.0.0.0:80 EXPOSE 80 From 65cc3123f4af47efb57b044bbd7978742beb64a9 Mon Sep 17 00:00:00 2001 From: Matthew Li Date: Fri, 31 Mar 2023 11:58:16 -0700 Subject: [PATCH 27/72] Correct bug in flag consistency check --- bootstrap/ansible/settings_template.tmpl | 3 ++- coldfront/templates/email/login/login_link.txt | 14 ++++++++++++++ .../email/login/login_link_ineligible.txt | 12 ++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 coldfront/templates/email/login/login_link.txt create mode 100644 coldfront/templates/email/login/login_link_ineligible.txt diff --git a/bootstrap/ansible/settings_template.tmpl b/bootstrap/ansible/settings_template.tmpl index 4f1557231..1d4798366 100644 --- a/bootstrap/ansible/settings_template.tmpl +++ b/bootstrap/ansible/settings_template.tmpl @@ -179,7 +179,8 @@ if not ( FLAGS['SSO_ENABLED'][0]['value']): raise ImproperlyConfigured( 'Exactly one of BASIC_AUTH_ENABLED, SSO_ENABLED should be enabled.') -if not FLAGS['SSO_ENABLED'][0]['value'] and FLAGS['LINK_LOGIN_ENABLED']: +if (not FLAGS['SSO_ENABLED'][0]['value'] and + FLAGS['LINK_LOGIN_ENABLED'][0]['value']): raise ImproperlyConfigured( 'LINK_LOGIN_ENABLED should only be enabled when SSO_ENABLED is ' 'enabled.') diff --git a/coldfront/templates/email/login/login_link.txt b/coldfront/templates/email/login/login_link.txt new file mode 100644 index 000000000..f42781534 --- /dev/null +++ b/coldfront/templates/email/login/login_link.txt @@ -0,0 +1,14 @@ +Dear {{ PORTAL_NAME }} user, + +You requested a link to log in to the portal. + +Below is the link, which will expire in {{ login_link_max_age_minutes }} minutes. + +{{ login_url }} + +Do not share this link with anyone else. + +Reminder: If your institution is listed in CILogon, you should use it to log in. + +Thank you, +{{ signature }} \ No newline at end of file diff --git a/coldfront/templates/email/login/login_link_ineligible.txt b/coldfront/templates/email/login/login_link_ineligible.txt new file mode 100644 index 000000000..7908590eb --- /dev/null +++ b/coldfront/templates/email/login/login_link_ineligible.txt @@ -0,0 +1,12 @@ +Dear {{ PORTAL_NAME }} user, + +You requested a link to log in to the portal. + +You are ineligible to receive a link for the following reason: + +{{ reason }} + +Please contact a system administrator if you have any questions. + +Thank you, +{{ signature }} \ No newline at end of file From 517ef7dca58a78c3bb7a9566ef1a9f2000394ae0 Mon Sep 17 00:00:00 2001 From: Viraat Chandra Date: Mon, 3 Apr 2023 01:33:06 -0700 Subject: [PATCH 28/72] address comments --- .../savio/project_request_surveys_modal.html | 58 ++++++++++--------- coldfront/core/project/views.py | 25 +++++--- 2 files changed, 47 insertions(+), 36 deletions(-) diff --git a/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html b/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html index 55f78cd36..66d020416 100644 --- a/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html +++ b/coldfront/core/project/templates/project/project_request/savio/project_request_surveys_modal.html @@ -20,11 +20,13 @@ @@ -33,31 +35,33 @@