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 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
10 changes: 7 additions & 3 deletions readthedocs/projects/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,13 @@ 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()
err_msg = ('Invalid project name, a <a href="{}" style="color: red">'
'project</a> already exists with that name').format(project_url)
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),
),

]
18 changes: 18 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,22 @@ 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_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_abandoned_mail/$',
private.send_abandoned_mail,
name='send_abandoned_mail'),

url(r'^(?P<project_slug>[-\w]+)/comments_moderation/$',
private.project_comments_moderation,
name='projects_comments_moderation'),
Expand Down
26 changes: 24 additions & 2 deletions readthedocs/projects/views/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from allauth.socialaccount.models import SocialAccount
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.http import (
Expand All @@ -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,27 @@ def is_advanced(self):
return data.get('advanced', True)


@login_required
@user_passes_test(lambda u: u.is_superuser)
def send_abandoned_mail(request, project_slug):
"""Sends abandoned project email."""
project = Project.objects.get(slug=project_slug)
proj_name = project_slug
context = {'proj_name': proj_name}
subject = 'Rename request for abandoned project'
for user in project.users.all():
email = user.email
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_abandoned_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_abandoned_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.abandoned_mail_sent %}
<form action="{% url "send_abandoned_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 %}