Skip to content

Added feature for sending abandoned project mail #3722

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 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 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
13 changes: 10 additions & 3 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,16 @@ def clean_name(self):
name = self.cleaned_data.get('name', '')
if not self.instance.pk:
potential_slug = slugify(name)
if Project.objects.filter(slug=potential_slug).exists():
raise forms.ValidationError(
_('Invalid project name, a project already exists with that name')) # yapf: disable # noqa
project_exist = Project.objects.filter(slug=potential_slug).exists()
if project_exist:
project = Project.objects.get(slug=potential_slug)
project_url = project.get_absolute_url()
if project.is_abandoned:
err_msg = ('Invalid project name, a <a href="{}" style="color: red">'
'project</a> already exists with that name').format(project_url)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to check for abandoned, we should just always link to the project here.

else:
err_msg = 'Invalid project name, a project already exists with that name'
raise forms.ValidationError(mark_safe(err_msg)) # yapf: disable # noqa
return name

def clean_repo(self):
Expand Down
21 changes: 21 additions & 0 deletions readthedocs/projects/migrations/0024_add_abandon_mail_sent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.12 on 2018-03-04 09:24
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('projects', '0023_migrate-alias-slug'),
]

operations = [
migrations.AddField(
model_name='project',
name='abandoned_mail_sent',
field=models.BooleanField(default=False),
),

]
23 changes: 23 additions & 0 deletions readthedocs/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import logging
import os
from builtins import object # pylint: disable=redefined-builtin
from datetime import datetime, timedelta

from django.conf import settings
from django.contrib.auth.models import User
Expand Down Expand Up @@ -82,6 +83,7 @@ class Project(models.Model):
related_name='projects')
name = models.CharField(_('Name'), max_length=255)
slug = models.SlugField(_('Slug'), max_length=255, unique=True)
abandoned_mail_sent = models.BooleanField(default=False)
description = models.TextField(_('Description'), blank=True,
help_text=_('The reStructuredText '
'description of the project'))
Expand Down Expand Up @@ -558,6 +560,27 @@ def conf_dir(self, version=LATEST):
if conf_file:
return os.path.dirname(conf_file)

@property
def is_abandoned(self):
"""Is project abandoned."""
if self.has_good_build:
latest_build = self.get_latest_build()
if latest_build:
latest_build_date = latest_build.date
today = datetime.today()
diff = today - latest_build_date
# Latest build a year ago.
if diff > timedelta(days=365):
return True
return False
return False
return True

@property
def is_abandoned_mail_sent(self):
"""Is abandoned mail sent."""
return self.abandoned_mail_sent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is necessary, you can just use obj.abandoned_mail_sent directly.


@property
def is_type_sphinx(self):
"""Is project type Sphinx."""
Expand Down
4 changes: 4 additions & 0 deletions readthedocs/projects/urls/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
private.project_manage,
name='projects_manage'),

url(r'^(?P<project_slug>[-\w]+)/send_mail/$',
private.send_mail,
name='send_mail'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a more explicit name & URL, send_abandoned_mail or similar


url(r'^(?P<project_slug>[-\w]+)/comments_moderation/$',
private.project_comments_moderation,
name='projects_comments_moderation'),
Expand Down
25 changes: 24 additions & 1 deletion readthedocs/projects/views/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
from readthedocs.builds.forms import AliasForm, VersionForm
from readthedocs.builds.models import Version, VersionAlias
from readthedocs.core.mixins import ListViewWithForm, LoginRequiredMixin
from readthedocs.core.utils import broadcast, trigger_build
from readthedocs.core.utils import broadcast, trigger_build, send_email
from readthedocs.core.permissions import AdminPermission
from readthedocs.integrations.models import HttpExchange, Integration
from readthedocs.oauth.services import registry
from readthedocs.oauth.utils import attach_webhook, update_webhook
Expand Down Expand Up @@ -264,6 +265,28 @@ def is_advanced(self):
return data.get('advanced', True)


@login_required
def send_mail(request, project_slug):
"""Sends abandoned project email."""
project = Project.objects.get(slug=project_slug)
proj_name = project_slug
for user in project.users.all():
if AdminPermission.is_admin(user, project):
email = user.email
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be done on another way (not sure how we can decide to which owner send the email)? Probably a break is needed here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I think this is insecure, since any logged in user can send a request here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made this button to be visible only to a superuser in the html template. Does that look fine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I have added a break statement. Should the mail be sent to a list of all users?

Copy link
Member

@stsewd stsewd Mar 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but any user could do a manual request, so probably is worth to check for the superuser here. I'm not sure what is the best way of do it anyway, probably the required_login decorator accepts a argument or there is a super_user_only decorator?

Maybe @humitos can help us here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure @stsewd! Thanks 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stsewd I tested this on my local instance and it works as expected!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericholscher Can you please guide me here!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it is checking for project owners who are admins. We just want to send the email to all project owners, but only let admins do it. We should be doing a request.user.is_superuser or similar check to determine if the logged in user is an admin, and then send the email to all project owners.

Copy link
Contributor Author

@bansalnitish bansalnitish Mar 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The button for sending mail is only visible to the admins i.e. I have done a request.user.is_superuser check in the html template. Also, we can check for superuser using a decorator like this: https://stackoverflow.com/questions/12003736/django-login-required-decorator-for-a-superuser. Does that look fine @ericholscher?

break
context = {'proj_name': proj_name}
subject = 'Rename request for abandoned project'
send_email(
recipient=email,
subject=subject,
template='projects/email/abandon_project.txt',
template_html='projects/email/abandon_project.html',
context=context)
project.abandoned_mail_sent = True
project.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))


class ImportDemoView(PrivateViewMixin, View):

"""View to pass request on to import form to import demo project."""
Expand Down
2 changes: 2 additions & 0 deletions readthedocs/rtd_tests/tests/test_privacy_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ class PrivateProjectAdminAccessTest(PrivateProjectMixin, TestCase):
# Places where we 302 on success -- These delete pages should probably be 405'ing
'/dashboard/import/manual/demo/': {'status_code': 302},
'/dashboard/pip/': {'status_code': 302},
'/dashboard/pip/send_mail/': {'status_code':302},
'/dashboard/pip/subprojects/delete/sub/': {'status_code': 302},
'/dashboard/pip/translations/delete/sub/': {'status_code': 302},

Expand Down Expand Up @@ -253,6 +254,7 @@ class PrivateProjectUserAccessTest(PrivateProjectMixin, TestCase):

# Unauth access redirect for non-owners
'/dashboard/pip/': {'status_code': 302},
'/dashboard/pip/send_mail/': {'status_code':302},

# 405's where we should be POST'ing
'/dashboard/pip/users/delete/': {'status_code': 405},
Expand Down
76 changes: 76 additions & 0 deletions readthedocs/rtd_tests/tests/test_project.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# -*- coding: utf-8 -*-
from __future__ import (
absolute_import, division, print_function, unicode_literals)
Expand Down Expand Up @@ -216,3 +217,78 @@ def test_finish_inactive_builds_task(self):
self.assertTrue(self.build_3.success)
self.assertEqual(self.build_3.error, '')
self.assertEqual(self.build_3.state, BUILD_STATE_TRIGGERED)


class TestAbandonedProject(TestCase):
fixtures = ['eric', 'test_data']

def setUp(self):
self.client.login(username='eric', password='test')
self.pip = Project.objects.get(slug='pip')
self.taggit = Project.objects.get(slug='taggit')
self.pinax = Project.objects.get(slug='pinax')

self.build_1 = Build.objects.create(
project=self.pip,
version=self.pip.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_1.date = (
datetime.datetime.now() - datetime.timedelta(days=750))
self.build_1.success = False
self.build_1.save()

self.build_2 = Build.objects.create(
project=self.pip,
version=self.pip.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_2.success = True
self.build_2.save()

self.build_3 = Build.objects.create(
project=self.taggit,
version=self.taggit.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_3.date = (
datetime.datetime.now() - datetime.timedelta(days=2))
self.build_3.success = False
self.build_3.save()

self.build_4 = Build.objects.create(
project=self.taggit,
version=self.taggit.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_4.success = False
self.build_4.save()

self.build_5 = Build.objects.create(
project=self.pinax,
version=self.pinax.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_5.success = False
self.build_5.save()

self.build_6 = Build.objects.create(
project=self.pinax,
version=self.pinax.get_stable_version(),
state=BUILD_STATE_FINISHED,
)

self.build_6.date = (
datetime.datetime.now() - datetime.timedelta(days=750))
self.build_6.success = True
self.build_6.save()

def test_abandoned_project(self):
self.assertFalse(self.pip.is_abandoned)
self.assertTrue(self.taggit.is_abandoned)
self.assertFalse(self.pinax.is_abandoned)
13 changes: 13 additions & 0 deletions readthedocs/templates/core/project_bar_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ <h1>
<a href="{{ project.get_absolute_url }}">{{ project }}</a>
</h1>
</div>

{% if project.is_abandoned and request.user.is_superuser %}
<div class="abandon=project">
{% if not project.is_abandoned_mail_sent %}
<form action="{% url "send_mail" project.slug %}" method="post">
{% csrf_token %}
<input type="submit" name="submit-btn" value="{% trans "Email project owner with our abandoned project email" %}"/>
</form>
{% else %}
<p><strong>{%trans "Abandonment mail was sent to the owner of the project." %}</strong></p>
{% endif %}
</div>
{% endif %}

<div class="options">

Expand Down
13 changes: 13 additions & 0 deletions readthedocs/templates/projects/email/abandon_project.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{% extends "core/email/common.html" %}

{% load i18n %}

{% block content %}
<p>
We've had a request from one of our users for the project name {{proj_name}} on Read the Docs. You are the current owner, and we wanted to reach out to you in accordance with our Abandoned Project policy (http://docs.readthedocs.io/en/latest/abandoned-projects.html).
</p>

<p>
Please reply at [email protected] either allowing or disallowing the transfer of the name on Read the Docs within 6 weeks, otherwise we will take the action of *initiating the transfer to a new owner* by default.
</p>
{% endblock %}
9 changes: 9 additions & 0 deletions readthedocs/templates/projects/email/abandon_project.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "core/email/common.txt" %}

{% load i18n %}

{% block content %}
We've had a request from one of our users for the project name {{proj_name}} on Read the Docs. You are the current owner, and we wanted to reach out to you in accordance with our Abandoned Project policy (http://docs.readthedocs.io/en/latest/abandoned-projects.html).

Please reply at [email protected] either allowing or disallowing the transfer of the name on Read the Docs within 6 weeks, otherwise we will take the action of *initiating the transfer to a new owner* by default.
{% endblock %}