Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

add sponsored user affiliation expiration mvp #3585

Merged
merged 10 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion perma_web/perma/celery_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@
from django.template.defaultfilters import pluralize, filesizeformat

from perma.models import LinkUser, Link, Capture, \
CaptureJob, InternetArchiveItem, InternetArchiveFile, Folder
CaptureJob, InternetArchiveItem, InternetArchiveFile, Folder, Sponsorship
from perma.exceptions import PermaPaymentsCommunicationException, ScoopAPINetworkException
from perma.utils import (
remove_whitespace,
get_ia_session, ia_global_task_limit_approaching,
ia_perma_task_limit_approaching, ia_bucket_task_limit_approaching,
copy_file_data, date_range, send_to_scoop, calculate_s3_etag)
from perma.email import send_user_email

import logging
logger = logging.getLogger('celery.django')
Expand Down Expand Up @@ -1515,3 +1516,54 @@ def format_filesize(i):
storages[settings.WACZ_STORAGE].save(link.warc_to_wacz_conversion_log_file(), StringIO(json.dumps(data)))
if data["error"] and warn_on_error:
logger.warning(data["error"])


def email_expiring_sponsored_user(user, context):
"""
Send email to sponsored user notifying about affiliation expiry
"""
send_user_email(
user.raw_email,
"email/sponsored_user_expiry_notification.txt",
context
)


@shared_task
def manage_sponsored_users_expiration(warning_periods):
"""
Notifies users whose sponsorship is expiring
Deactivates those whose sponsorship expired
"""
warning_periods.sort(reverse=True)
todays_date = timezone.now().date()
max_notification_date = todays_date + timedelta(days=warning_periods[0])
expiring_sponsorships = (
Sponsorship.objects.filter(status="active", expires_at__lte=max_notification_date)
.select_related('user')
.select_related('registrar')
)

if not expiring_sponsorships:
return

for sponsorship in expiring_sponsorships:
expiration_date = sponsorship.expires_at.date()
if expiration_date < todays_date:
logger.info(f"Deactivating user {sponsorship.user_id}")
sponsorship.status = "inactive"
sponsorship.save()
continue

for period in warning_periods:
warning_date = todays_date + timedelta(days=period)
if expiration_date == warning_date:
context = {
"registrar_name": sponsorship.registrar.name,
"registrar_email": sponsorship.registrar.email,
"expiration_days": period,
"expiration_date": expiration_date + timedelta(days=1)
}
email_expiring_sponsored_user(sponsorship.user, context)
break

14 changes: 12 additions & 2 deletions perma_web/perma/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,16 @@ class UserFormWithSponsoringRegistrar(UserForm):
add sponsoring registrar to the create user form
"""
sponsoring_registrars = forms.ModelChoiceField(label='Sponsoring Registrar', queryset=Registrar.objects.approved().order_by('name'))
indefinite_sponsorship = forms.BooleanField(
label="Sponsor indefinitely",
required=False,
initial=True
)
expires_at = forms.DateTimeField(
label="Sponsorship expiration date",
widget=forms.DateTimeInput(attrs={"type": "date"}),
required=False
)

def __init__(self, data=None, current_user=None, **kwargs):
self.current_user = current_user
Expand All @@ -213,7 +223,7 @@ def __init__(self, data=None, current_user=None, **kwargs):

class Meta:
model = LinkUser
fields = ["first_name", "last_name", "email", "sponsoring_registrars"]
fields = ["first_name", "last_name", "email", "sponsoring_registrars", "indefinite_sponsorship", "expires_at"]

def clean(self):
super().clean()
Expand All @@ -229,7 +239,7 @@ def save(self, commit=True):
# Adapted from https://stackoverflow.com/a/2264722
instance = forms.ModelForm.save(self, False)
def save_m2m():
Sponsorship.objects.create(registrar=self.cleaned_data['sponsoring_registrars'], user=instance, created_by=self.current_user)
Sponsorship.objects.create(registrar=self.cleaned_data['sponsoring_registrars'], user=instance, created_by=self.current_user, expires_at=self.cleaned_data['expires_at'])
self.save_m2m = save_m2m
if commit:
instance.save()
Expand Down
18 changes: 18 additions & 0 deletions perma_web/perma/migrations/0047_sponsorship_expires_at.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.14 on 2024-07-31 12:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("perma", "0046_capturejob_archive_formats"),
]

operations = [
migrations.AddField(
model_name="sponsorship",
name="expires_at",
field=models.DateTimeField(blank=True, null=True),
),
]
1 change: 1 addition & 0 deletions perma_web/perma/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ class Sponsorship(models.Model):
status_changed = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
created_by = models.ForeignKey('LinkUser', related_name='created_sponsorships', on_delete=models.PROTECT)
expires_at = models.DateTimeField(blank=True, null=True)

class Meta:
constraints = [
Expand Down
3 changes: 2 additions & 1 deletion perma_web/perma/settings/deployments/settings_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
'perma.celery_tasks.confirm_file_deleted_from_daily_item': {'queue': 'ia-readonly'},
'perma.celery_tasks.conditionally_queue_internet_archive_uploads_for_date_range': {'queue': 'ia-readonly'},
'perma.celery_tasks.queue_internet_archive_deletions': {'queue': 'ia-readonly'},
'perma.celery_tasks.convert_warc_to_wacz': {'queue': 'wacz-conversion'}
'perma.celery_tasks.convert_warc_to_wacz': {'queue': 'wacz-conversion'},
'perma.celery_tasks.manage_sponsored_users_expiration': {'queue': 'background'}
}

# Schedule celerybeat jobs.
Expand Down
1 change: 1 addition & 0 deletions perma_web/perma/settings/deployments/settings_prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'conditionally_queue_internet_archive_uploads_for_date_range',
'confirm_files_uploaded_to_internet_archive',
'confirm_files_deleted_from_internet_archive',
'manage_sponsored_users_expiration'
]

# logging
Expand Down
5 changes: 5 additions & 0 deletions perma_web/perma/settings/utils/post_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ def post_process_settings(settings):
'confirm_files_deleted_from_internet_archive': {
'task': 'perma.celery_tasks.queue_file_deleted_confirmation_tasks',
'schedule': crontab(minute="2-59/5"),
},
'manage_sponsored_users_expiration': {
'task': 'perma.celery_tasks.manage_sponsored_users_expiration',
'schedule': crontab(hour='6', minute='0'),
'args': ([7, 15, 30],)
}
}
settings['CELERY_BEAT_SCHEDULE'] = dict(((job, celerybeat_job_options[job]) for job in settings.get('CELERY_BEAT_JOB_NAMES', [])),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
TITLE: Your sponsored Perma.cc account is expiring

Your Perma.cc sponsorship via {{ registrar_name }} is expiring in {{ expiration_days }} days. Your Sponsored Links folder will become read-only on {{ expiration_date }}. You will retain access to your dashboard and all previously made Perma Links.

If you would like to request an extension of your affiliation please reach out to {{ registrar_email }}.

Thanks,
The Perma.cc Team
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ <h4 class="body-ch">{{ status.grouper|title }}</h4>
{% else %}
<a href="{% url 'user_management_manage_single_sponsored_user_readd' user_id=target_user.id registrar_id=sponsorship.registrar.id %}" name="registrar" class="btn btn-default btn-xs leave-org-btn">Reactivate</a>
{% endif %}
{% if sponsorship.expires_at %}
<br><p>Expiration date: {{ sponsorship.expires_at }}</p>
{% endif %}
</div>
{% endfor %}
{% endfor %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ <h2 class="body-ah _hideMobile">Profile</h2>
<button type="submit" class="btn">Save changes</button>
</form>

{% if request.user.sponsorships.first %}
<h2 class="body-ah">Sponsorships</h2>
{% for sponsorship in request.user.sponsorships.all %}
<p>{{ sponsorship.registrar.name }}</p>
{% if sponsorship.expires_at %}
<p>Expiration date: {{ sponsorship.expires_at }}</p>
{% endif %}
{% endfor %}
{% endif %}
{% if "Requested account deletion" in request.user.notes %}
<h2 class="body-ah">Deletion Request Received</h2>
<p>We've received the request to delete your account. We're sorry to see you go!</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ <h3 class="body-bh">{% if request.user.is_registrar_user %}Sponsor {{ user_email
<button type="submit" class="btn">{% if request.user.is_registrar_user %}Sponsor{% else %}Add sponsorship{% endif %}</button>
<a href="{% url 'user_management_manage_sponsored_user' %}" class="btn cancel">Cancel</a>
</form>
<script>
const checkbox = document.getElementById("id_a-indefinite_sponsorship");
const datetimeField = document.getElementById("id_a-expires_at");
const datetimeFieldLabel = document.querySelector('label[for="id_a-expires_at"]');

const toggleExpirationDateField = () => {
const displayStyle = checkbox.checked ? "none" : "block";
datetimeField.style.display = displayStyle;
datetimeFieldLabel.style.display = displayStyle;
};

document.addEventListener("DOMContentLoaded", toggleExpirationDateField);
checkbox.addEventListener("change", toggleExpirationDateField);
</script>

{% endif %}
{% endblock %}
Expand Down
7 changes: 7 additions & 0 deletions perma_web/static/bundles/global.css

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

2 changes: 1 addition & 1 deletion perma_web/static/bundles/global.css.map

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions perma_web/static/css/style-responsive.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,13 @@ div.checkbox {
padding-left: 36px;
}

div.checkbox:has(#id_a-indefinite_sponsorship) {
background-color: inherit;
padding-left: 0;
margin-right: 0;
margin-top: $grid * 2;
}

body.single-registrar div.checkbox {
background-color: initial;
}
Expand Down