Skip to content

Commit

Permalink
Merge branch 'notifications'
Browse files Browse the repository at this point in the history
  • Loading branch information
ericholscher committed Dec 18, 2012
2 parents 21cab58 + ffc3c71 commit 1f7b792
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 7 deletions.
4 changes: 3 additions & 1 deletion readthedocs/projects/admin.py
Expand Up @@ -4,7 +4,7 @@

from builds.models import Version
from django.contrib import admin
from projects.models import Project, ImportedFile, ProjectRelationship
from projects.models import Project, ImportedFile, ProjectRelationship, EmailHook, WebHook
from guardian.admin import GuardedModelAdmin

class ProjectRelationshipInline(admin.TabularInline):
Expand All @@ -31,3 +31,5 @@ class ImportedFileAdmin(admin.ModelAdmin):

admin.site.register(Project, ProjectAdmin)
admin.site.register(ImportedFile, ImportedFileAdmin)
admin.site.register(EmailHook)
admin.site.register(WebHook)
17 changes: 16 additions & 1 deletion readthedocs/projects/forms.py
Expand Up @@ -7,7 +7,7 @@
from django.utils.safestring import mark_safe

from projects import constants
from projects.models import Project
from projects.models import Project, EmailHook, WebHook
from projects.tasks import update_docs


Expand Down Expand Up @@ -221,3 +221,18 @@ def clean_user(self):
def save(self):
project = self.project.users.add(self.user)
return self.user

class EmailHookForm(forms.Form):
email = forms.EmailField()

def __init__(self, *args, **kwargs):
self.project = kwargs.pop('project', None)
super(EmailHookForm, self).__init__(*args, **kwargs)

def clean_email(self):
self.email = EmailHook.objects.get_or_create(email=self.cleaned_data['email'], project=self.project)[0]
return self.email

def save(self):
project = self.project.emailhook_notifications.add(self.email)
return self.project
159 changes: 159 additions & 0 deletions readthedocs/projects/migrations/0030_auto__add_webhook.py
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models


class Migration(SchemaMigration):

def forwards(self, orm):
# Adding model 'WebHook'
db.create_table('projects_webhook', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='webhook_notifications', to=orm['projects.Project'])),
('url', self.gf('django.db.models.fields.URLField')(max_length=200, blank=True)),
))
db.send_create_signal('projects', ['WebHook'])
# Adding model 'EmailHook'
db.create_table('projects_emailhook', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='emailhook_notifications', to=orm['projects.Project'])),
('email', self.gf('django.db.models.fields.EmailField')(max_length=75)),
))
db.send_create_signal('projects', ['EmailHook'])


def backwards(self, orm):
# Deleting model 'WebHook'
db.delete_table('projects_webhook')
# Deleting model 'EmailHook'
db.delete_table('projects_emailhook')


models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'projects.file': {
'Meta': {'ordering': "('denormalized_path',)", 'object_name': 'File'},
'content': ('django.db.models.fields.TextField', [], {}),
'denormalized_path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ordering': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '1'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': "orm['projects.File']"}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'files'", 'to': "orm['projects.Project']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'status': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '1'})
},
'projects.filerevision': {
'Meta': {'ordering': "('-revision_number',)", 'object_name': 'FileRevision'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'diff': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': "orm['projects.File']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_reverted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'revision_number': ('django.db.models.fields.IntegerField', [], {})
},
'projects.importedfile': {
'Meta': {'object_name': 'ImportedFile'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'md5': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'imported_files'", 'to': "orm['projects.Project']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'})
},
'projects.project': {
'Meta': {'ordering': "('slug',)", 'object_name': 'Project'},
'analytics_code': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
'conf_py_file': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'copyright': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'crate_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'default_branch': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'default_version': ('django.db.models.fields.CharField', [], {'default': "'latest'", 'max_length': '255'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'django_packages_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'documentation_type': ('django.db.models.fields.CharField', [], {'default': "'sphinx'", 'max_length': '20'}),
'featured': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'project_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
'pub_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'related_projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['projects.Project']", 'null': 'True', 'through': "orm['projects.ProjectRelationship']", 'blank': 'True'}),
'repo': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'repo_type': ('django.db.models.fields.CharField', [], {'default': "'git'", 'max_length': '10'}),
'requirements_file': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
'skip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
'suffix': ('django.db.models.fields.CharField', [], {'default': "'.rst'", 'max_length': '10'}),
'theme': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '20'}),
'use_virtualenv': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'projects'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
'version': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'})
},
'projects.projectrelationship': {
'Meta': {'object_name': 'ProjectRelationship'},
'child': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'superprojects'", 'to': "orm['projects.Project']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'subprojects'", 'to': "orm['projects.Project']"})
},
'projects.webhook': {
'Meta': {'object_name': 'WebHook'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'webhook_notifications'", 'to': "orm['projects.Project']"}),
'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
},
'taggit.tag': {
'Meta': {'object_name': 'Tag'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'})
},
'taggit.taggeditem': {
'Meta': {'object_name': 'TaggedItem'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"})
}
}

complete_apps = ['projects']
25 changes: 24 additions & 1 deletion readthedocs/projects/models.py
Expand Up @@ -508,7 +508,7 @@ def get_default_branch(self):
"""
if self.default_branch:
return self.default_branch
else:
else:
return self.vcs_repo().fallback_branch

def add_subproject(self, child):
Expand Down Expand Up @@ -539,3 +539,26 @@ def get_absolute_url(self):

def __unicode__(self):
return '%s: %s' % (self.name, self.project)


class Notification(models.Model):
project = models.ForeignKey(Project,
related_name='%(class)s_notifications')

class Meta:
abstract = True


class EmailHook(Notification):
email = models.EmailField()

def __unicode__(self):
return self.email


class WebHook(Notification):
url = models.URLField(blank=True, verify_exists=False,
help_text=_('URL to send the webhook to'))

def __unicode__(self):
return self.url
50 changes: 48 additions & 2 deletions readthedocs/projects/tasks.py
Expand Up @@ -222,7 +222,7 @@ def new_save(*args, **kwargs):
purge_version(version, subdomain=True,
mainsite=True, cname=True)
symlink_cname(version)
send_notifications(version)
send_notifications(version, build)
log.info("Purged %s" % version)
else:
log.warning("Failed HTML Build")
Expand Down Expand Up @@ -559,8 +559,54 @@ def symlink_cname(version):
run_on_app_servers('mkdir -p %s' % '/'.join(symlink.split('/')[:-1]))
run_on_app_servers('ln -nsf %s %s' % (build_dir, symlink))

def send_notifications(version, build):
zenircbot_notification(version.id)
for hook in version.project.webhook_notifications.all():
webhook_notification.delay(version.project.id, build, hook.url)
emails = version.project.emailhook_notifications.all().values_list('email',
flat=True)
for email in emails:
email_notification(version.project.id, build, email)

def send_notifications(version):

@task
def email_notification(project_id, build, email):
if build['success']:
return
project = Project.objects.get(id=project_id)
build_obj = Build.objects.get(id=build['id'])
subject = (_('(ReadTheDocs) Building docs for %s failed') % project.name)
template = 'projects/notification_email.txt'
context = {
'project': project.name,
'build_url': 'http://%s%s' % (Site.objects.get_current().domain,
build_obj.get_absolute_url())
}
message = get_template(template).render(Context(context))

send_mail(subject=subject, message=message,
from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=(email,))


@task
def webhook_notification(project_id, build, hook_url):
project = Project.objects.get(id=project_id)
data = json.dumps({
'name': project.name,
'slug': project.slug,
'build': {
'id': build['id'],
'success': build['success'],
'date': build['date']
}
})
log.debug('sending notification to: %s' % hook_url)
requests.post(hook_url, data=data)


@task
def zenircbot_notification(version_id):
version = Version.objects.get(id=version_id)
message = "Build of %s successful" % version
redis_obj = redis.Redis(**settings.REDIS)
IRC = getattr(settings, 'IRC_CHANNEL', '#readthedocs-build')
Expand Down
8 changes: 8 additions & 0 deletions readthedocs/projects/urls/private.py
Expand Up @@ -61,4 +61,12 @@
'project_users_delete',
name='projects_users_delete'
),
url(r'^(?P<project_slug>[-\w]+)/notifications/$',
'project_notifications',
name='projects_notifications'
),
url(r'^(?P<project_slug>[-\w]+)/notifications/delete/$',
'project_notifications_delete',
name='projects_notification_delete'
),
)
32 changes: 30 additions & 2 deletions readthedocs/projects/views/private.py
Expand Up @@ -18,8 +18,8 @@
from builds.models import Version
from projects.forms import (ImportProjectForm, build_versions_form,
build_upload_html_form, SubprojectForm,
UserForm)
from projects.models import Project
UserForm, EmailHookForm)
from projects.models import Project, EmailHook, WebHook
from projects.tasks import unzip_files
from projects import constants

Expand Down Expand Up @@ -296,3 +296,31 @@ def project_users_delete(request, project_slug):
project.users.remove(user)
project_dashboard = reverse('projects_users', args=[project.slug])
return HttpResponseRedirect(project_dashboard)

@login_required
def project_notifications(request, project_slug):
project = get_object_or_404(request.user.projects.live(), slug=project_slug)
form = EmailHookForm(data=request.POST or None, project=project)

if request.method == 'POST' and form.is_valid():
form.save()
project_dashboard = reverse('projects_notifications', args=[project.slug])
return HttpResponseRedirect(project_dashboard)

emails = project.emailhook_notifications.all()

return render_to_response(
'projects/project_notifications.html',
{'form': form, 'project': project, 'emails': emails},
context_instance=RequestContext(request)
)

@login_required
def project_notifications_delete(request, project_slug):
if request.method != 'POST':
raise Http404
project = get_object_or_404(request.user.projects.live(), slug=project_slug)
notification = get_object_or_404(EmailHook.objects.all(), email=request.POST.get('email'))
notification.delete()
project_dashboard = reverse('projects_notifications', args=[project.slug])
return HttpResponseRedirect(project_dashboard)
3 changes: 3 additions & 0 deletions readthedocs/settings/sqlite.py
Expand Up @@ -41,6 +41,9 @@
IMPORT_EXTERNAL_DATA = False
DONT_HIT_DB = False

CELERY_ALWAYS_EAGER = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

try:
from local_settings import *
except:
Expand Down
10 changes: 10 additions & 0 deletions readthedocs/templates/projects/notification_email.txt
@@ -0,0 +1,10 @@
Hello!

Unfortunately, we have to inform you that your docs have failed to build. You
can see what went wrong at: {{ build_url }}

If you have questions, a good place to start is the FAQ:
http://read-the-docs.readthedocs.org/en/latest/faq.html

Thanks,
The happy ReadTheDocs servers

0 comments on commit 1f7b792

Please sign in to comment.