Skip to content

Notifications: email about DONT_SHALLOW_CLONE deprecated feature flag #10463

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 60 additions & 60 deletions readthedocs/core/management/commands/contact_owners.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import sys
from pathlib import Path
from pprint import pprint

import structlog
from django.conf import settings
Expand Down Expand Up @@ -49,15 +48,19 @@ class Command(BaseCommand):
to be included in the notification.
* ``email.md`` is a Markdown file with the first line as the subject,
and the rest is the content.
Note that ``user`` and ``domain`` are available in the context.

The context available is:
* ``user``
* ``projects``
* ``production_uri``

.. code:: markdown

Read the Docs deprecated option, action required

Dear {{ user.firstname }},

Greetings from [Read the Docs]({{ domain }}).
Greetings from [Read the Docs]({{ production_uri }}).

.. note::

Expand All @@ -77,30 +80,30 @@ class Command(BaseCommand):

def add_arguments(self, parser):
parser.add_argument(
'--production',
action='store_true',
dest='production',
"--production",
action="store_true",
dest="production",
default=False,
help=(
'Send the email/notification for real, '
'otherwise we only print the notification in the console (dryrun).'
)
"Send the email/notification for real, "
"otherwise we only logs the notification in the console (dryrun)."
),
)
parser.add_argument(
'--email',
"--email",
help=(
'Path to a file with the email content in markdown. '
'The first line would be the subject.'
"Path to a file with the email content in markdown. "
"The first line would be the subject."
),
)
parser.add_argument(
'--notification',
help='Path to a file with the notification content in markdown.',
"--notification",
help="Path to a file with the notification content in markdown.",
)
parser.add_argument(
'--sticky',
action='store_true',
dest='sticky',
"--sticky",
action="store_true",
dest="sticky",
default=False,
help=(
'Make the notification sticky '
Expand Down Expand Up @@ -143,7 +146,7 @@ def handle(self, *args, **options):
users = AdminPermission.owners(organization)
elif usernames:
file = Path(usernames)
with file.open() as f:
with file.open(encoding="utf8") as f:
usernames = f.readlines()

# remove "\n" from lines
Expand All @@ -155,62 +158,59 @@ def handle(self, *args, **options):
organizationowner__organization__disabled=False
).distinct()
else:
users = (
User.objects
.filter(projects__skip=False)
.distinct()
)

print(
"len(owners)={} production={} email={} notification={} sticky={}".format(
users.count(),
bool(options["production"]),
options["email"],
options["notification"],
options["sticky"],
)
users = User.objects.filter(projects__skip=False).distinct()

log.info(
"Command arguments.",
n_owners=users.count(),
production=bool(options["production"]),
email_filepath=options["email"],
notification_filepath=options["notification"],
sticky=options["sticky"],
)

if input("Continue? y/N: ") != "y":
print("Aborting run.")
return

notification_content = ''
if options['notification']:
file = Path(options['notification'])
with file.open() as f:
notification_content = ""
if options["notification"]:
file = Path(options["notification"])
with file.open(encoding="utf8") as f:
notification_content = f.read()

email_subject = ''
email_content = ''
if options['email']:
file = Path(options['email'])
with file.open() as f:
content = f.read().split('\n')
email_subject = ""
email_content = ""
if options["email"]:
file = Path(options["email"])
with file.open(encoding="utf8") as f:
content = f.read().split("\n")
email_subject = content[0].strip()
email_content = '\n'.join(content[1:]).strip()
email_content = "\n".join(content[1:]).strip()

resp = contact_users(
users=users,
email_subject=email_subject,
email_content=email_content,
notification_content=notification_content,
sticky_notification=options['sticky'],
dryrun=not options['production'],
sticky_notification=options["sticky"],
dryrun=not options["production"],
)

email = resp["email"]
log.info(
"Sending emails finished.",
total=len(email["sent"]),
total_failed=len(email["failed"]),
sent_emails=email["sent"],
failed_emails=email["failed"],
)

email = resp['email']
total = len(email['sent'])
total_failed = len(email['failed'])
print(f'Emails sent ({total}):')
pprint(email['sent'])
print(f'Failed emails ({total_failed}):')
pprint(email['failed'])

notification = resp['notification']
total = len(notification['sent'])
total_failed = len(notification['failed'])
print(f'Notifications sent ({total})')
pprint(notification['sent'])
print(f'Failed notifications ({total_failed})')
pprint(notification['failed'])
notification = resp["notification"]
log.info(
"Sending notifications finished.",
total=len(notification["sent"]),
total_failed=len(notification["failed"]),
sent_notifications=notification["sent"],
failed_notifications=notification["failed"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[Action required] Unshallow your clone via the configuration file

{% load projects_tags %}
Some time ago we added an on-request "feature flag" to allow users to unshallow their Git repository
when clonning them as part of the build process.
With the introduction of the ability to [customize the build process](https://docs.readthedocs.io/en/latest/build-customization.html) via `build.jobs` and `build.commands`,
this feature flag is not required anymore and we are deprecating it.
Now, users have the ability to unshallow the Git repository clone without contacting support.

We are sending you this email because you are a maintainer of the following projects that have
this "feature flag" enabled and you should unshallow your clone now by using the configuration file:

{% spaceless %}
{% for project in projects %}
{% if project|has_feature:"dont_shallow_clone" %}
* [{{ production_uri }}{{ project.get_absolute_url }}]({{ project.slug }})
{% endif %}
{% endfor %}
{% endspaceless %}

Note this feature flag will be completely removed on **August 28th**.
Please, refer to [the example we have in the documentation](https://docs.readthedocs.io/en/latest/build-customization.html#unshallow-git-clone)
to unshallow your clone via the configuration file if your projects still require it.
42 changes: 17 additions & 25 deletions readthedocs/core/utils/contact.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import structlog
from pprint import pprint

import markdown
from django.conf import settings
from django.core.mail import send_mail
from django.template import Context, Engine
from messages_extends import constants as message_constants

from readthedocs.core.permissions import AdminPermission
from readthedocs.notifications import SiteNotification
from readthedocs.notifications.backends import SiteBackend

Expand All @@ -33,14 +33,15 @@ def contact_users(
:param string notification_content: Content for the sticky notification (markdown)
:param context_function: A callable that will receive an user
and return a dict of additional context to be used in the email/notification content
:param bool dryrun: If `True` don't sent the email or notification, just print the content
:param bool dryrun: If `True` don't sent the email or notification, just logs the content

The `email_content` and `notification_content` contents will be rendered using
a template with the following context::

{
'user': <user object>,
'domain': https://readthedocs.org,
'production_uri': https://readthedocs.org,
'projects': QuerySet(<project object>),
}

:returns: A dictionary with a list of sent/failed emails/notifications.
Expand All @@ -67,41 +68,34 @@ class TempNotification(SiteNotification):
success_level = message_constants.SUCCESS_PERSISTENT

def render(self, *args, **kwargs):
context = {
'user': self.user,
'domain': f'https://{settings.PRODUCTION_DOMAIN}',
}
context.update(context_function(self.user))
return markdown.markdown(
notification_template.render(Context(context))
notification_template.render(Context(self.get_context_data()))
)

total = users.count()
for count, user in enumerate(users.iterator(), start=1):
context = {
'user': user,
'domain': f'https://{settings.PRODUCTION_DOMAIN}',
"user": user,
"production_uri": f"https://{settings.PRODUCTION_DOMAIN}",
"projects": AdminPermission.projects(user),
}
context.update(context_function(user))

if notification_content:
notification = TempNotification(
user=user,
success=True,
extra_context=context,
)
try:
if not dryrun:
backend.send(notification)
else:
pprint(markdown.markdown(
notification_template.render(Context(context))
))
except Exception:
log.exception('Notification failed to send')
failed_notifications.add(user.username)
else:
log.info(
'Successfully set notification.',
"Successfully sent notification.",
user_username=user.username,
count=count,
total=total,
Expand Down Expand Up @@ -132,17 +126,15 @@ def render(self, *args, **kwargs):
)

try:
kwargs = dict(
subject=email_subject,
message=email_txt_rendered,
html_message=email_html_rendered,
from_email=from_email,
recipient_list=emails,
)
kwargs = {
"subject": email_subject,
"message": email_txt_rendered,
"html_message": email_html_rendered,
"from_email": from_email,
"recipient_list": emails,
}
if not dryrun:
send_mail(**kwargs)
else:
pprint(kwargs)
except Exception:
log.exception('Email failed to send')
failed_emails.update(emails)
Expand Down
6 changes: 6 additions & 0 deletions readthedocs/projects/templatetags/projects_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ def sort_version_aware(versions):
def is_project_user(user, project):
"""Checks if the user has access to the project."""
return user in AdminPermission.members(project)


@register.filter
def has_feature(project, feature):
"""Check if the project has a particular feature flag."""
return project.has_feature(feature)