Skip to content
Browse files

[Bug 790798] Front end for locale announcements.

Users that have permission can create locale announcements from the
locale dashboard.
  • Loading branch information...
1 parent 0e7b4cc commit f67ea61de6b028db41b0432dd221e1023d551e7c @mythmon mythmon committed
View
22 apps/announcements/forms.py
@@ -0,0 +1,22 @@
+from datetime import date
+
+from django import forms
+
+from tower import ugettext as _
+
+
+class AnnouncementForm(forms.Form):
+ """Form for collecting information about an announcement.
+
+ This is not a ModelForm, and does not include the group or locale fields,
+ because it should only be used in a context where the group or locale is
+ implicit, and should not be user controllable. If you need a user
+ controllable locale or group, use the admin interface.
+
+ """
+ content = forms.CharField(label=_('Content'), max_length=10000,
+ widget=forms.Textarea)
+ show_after = forms.DateField(label=_('Show after'), initial=date.today,
+ input_formats=['%Y-%m-%d'])
+ show_until = forms.DateField(label=_('Show until'), required=False,
+ input_formats=['%Y-%m-%d'])
View
98 apps/announcements/tests/test_views.py
@@ -0,0 +1,98 @@
+from datetime import datetime
+
+from nose.tools import eq_
+
+from announcements.models import Announcement
+from announcements.tests import announcement
+from sumo.tests import TestCase
+from sumo.urlresolvers import reverse
+from users.tests import user, add_permission
+from wiki.tests import locale
+
+
+class TestCreateLocaleAnnouncement(TestCase):
+
+ def setUp(self):
+ self.locale = locale(save=True, locale='es')
+
+ def _create_test(self, status, count):
+ """Login, or other setup, then call this."""
+ url = reverse('announcements.create_for_locale', locale='es')
+ resp = self.client.post(url, {
+ 'content': 'Look at me!',
+ 'show_after': '2012-01-01',
+ })
+ eq_(resp.status_code, status)
+ eq_(Announcement.objects.count(), count)
+
+ def test_create(self):
+ u = user(save=True, is_superuser=1)
+ self.client.login(username=u.username, password='testpass')
+ self._create_test(200, 1)
+
+ def test_leader(self):
+ u = user(save=True)
+ self.locale.leaders.add(u)
+ self.locale.save()
+ self.client.login(username=u.username, password='testpass')
+ self._create_test(200, 1)
+
+ def test_has_permission(self):
+ u = user(save=True)
+ add_permission(u, Announcement, 'add_announcement')
+ self.client.login(username=u.username, password='testpass')
+ self._create_test(200, 1)
+
+ def test_no_perms(self):
+ u = user(save=True)
+ self.client.login(username=u.username, password='testpass')
+ self._create_test(403, 0)
+
+ def test_anon(self):
+ self._create_test(302, 0)
+
+
+class TestDeleteAnnouncement(TestCase):
+
+ def setUp(self):
+ self.locale = locale(save=True, locale='es')
+
+ self.u = user(save=True)
+
+ self.locale.leaders.add(self.u)
+ self.locale.save()
+
+ self.announcement = announcement(save=True, creator=self.u,
+ locale=self.locale, content="Look at me!",
+ show_after=datetime(2012, 01, 01, 0, 0, 0))
+
+ def _delete_test(self, id, status, count):
+ """Login, or other setup, then call this."""
+ url = reverse('announcements.delete', locale='es', args=(id,))
+ resp = self.client.post(url)
+ eq_(resp.status_code, status)
+ eq_(Announcement.objects.count(), count)
+
+ def test_delete(self):
+ u = user(save=True, is_superuser=1)
+ self.client.login(username=u.username, password='testpass')
+ self._delete_test(self.announcement.id, 204, 0)
+
+ def test_leader(self):
+ # Use the user that was created in setUp.
+ self.client.login(username=self.u.username, password='testpass')
+ self._delete_test(self.announcement.id, 204, 0)
+
+ def test_has_permission(self):
+ u = user(save=True)
+ add_permission(u, Announcement, 'add_announcement')
+ self.client.login(username=u.username, password='testpass')
+ self._delete_test(self.announcement.id, 204, 0)
+
+ def test_no_perms(self):
+ u = user(save=True)
+ self.client.login(username=u.username, password='testpass')
+ self._delete_test(self.announcement.id, 403, 1)
+
+ def test_anon(self):
+ self._delete_test(self.announcement.id, 302, 1)
View
9 apps/announcements/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import patterns, url
+
+
+urlpatterns = patterns('announcements.views',
+ url(r'^/create/locale$', 'create_for_locale',
+ name='announcements.create_for_locale'),
+ url(r'^/(?P<announcement_id>\d+)/delete$', 'delete',
+ name='announcements.delete'),
+)
View
55 apps/announcements/views.py
@@ -0,0 +1,55 @@
+import json
+
+from django.http import HttpResponse, HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.views.decorators.http import require_POST
+from access.decorators import login_required
+
+from announcements.forms import AnnouncementForm
+from announcements.models import Announcement
+from wiki.models import Locale
+
+
+@require_POST
+@login_required
+def create_for_locale(request):
+ """An ajax view to create a new announcement for the current locale."""
+ user = request.user
+ locale = Locale.objects.get(locale=request.locale)
+
+ if not user_can_announce(user, locale):
+ return HttpResponseForbidden()
+
+ form = AnnouncementForm(request.POST)
+
+ if form.is_valid():
+ a = Announcement(creator=user, locale=locale, **form.cleaned_data)
+ a.save()
+ return HttpResponse(json.dumps({'id': a.id}),
+ content_type="application/json")
+ else:
+ return HttpResponse(json.dumps(form.errors), status=400,
+ content_type="application/json")
+
+
+@require_POST
+@login_required
+def delete(request, announcement_id):
+ """An ajax view to delete an announcement."""
+ user = request.user
+ locale = Locale.objects.get(locale=request.locale)
+
+ if not user_can_announce(user, locale):
+ return HttpResponseForbidden()
+
+ a = get_object_or_404(Announcement, id=announcement_id)
+ a.delete()
+
+ return HttpResponse("", status=204)
+
+
+def user_can_announce(user, locale):
+ if user.is_anonymous():
+ return False
+ return (user.has_perm('announcements.add_announcement') or
+ user in locale.leaders.all())
View
33 apps/dashboards/templates/dashboards/localization.html
@@ -18,12 +18,43 @@
{% for a in announcements %}
<li>
{{ a.content|wiki_to_html }}
- <p>{{ datetimeformat(a.show_after) }}</p>
+ <p>
+ {{ datetimeformat(a.show_after, 'date') }}
+ {% if user_can_announce %}
+ <a href="{{ url('announcements.delete', a.id) }}" class="delete">
+ {{ _('Delete') }}
+ </a>
+ {% endif %}
+ </p>
</li>
{% endfor %}
</ul>
{% endif %}
+ {% if user_can_announce %}
+ <div id="create-announcement">
+ <button class="btn">
+ <img src="{{ MEDIA_URL }}img/blank.png" />
+ {{ _('Create announcement') }}
+ </button>
+ <span class="success">{{ _('Created successfully') }}</span>
+ <div class="kbox" data-target="#create-announcement > .btn" data-position="none" data-close-on-out-click="true">
+ <form action="{{ url('announcements.create_for_locale') }}" method="POST">
+ {{ csrf() }}
+ <ul>
+ {{ announce_form.as_ul() }}
+ <li>
+ <button type="submit" class="btn btn-submit" value="Submit">
+ {{ _('Create') }}
+ </button>
+ <img src="{{ MEDIA_URL }}img/wait-trans.gif" class="spinner" />
+ </i>
+ </ul>
+ </form>
+ </div>
+ </div>
+ {% endif %}
+
<div class="choice-list">
<label>{{ _('Show information for:') }}</label>
<ul>
View
44 apps/dashboards/tests/test_views.py
@@ -1,4 +1,4 @@
-from datetime import timedelta
+from datetime import timedelta, datetime
import json
from django.conf import settings
@@ -6,6 +6,7 @@
from nose import SkipTest
from nose.tools import eq_
+from announcements.tests import announcement
from dashboards.cron import cache_most_unhelpful_kb_articles
from dashboards.readouts import CONTRIBUTOR_READOUTS
from sumo.tests import TestCase
@@ -13,7 +14,7 @@
from sumo.redis_utils import redis_client, RedisError
from users.tests import user, group
from wiki.models import HelpfulVote
-from wiki.tests import revision
+from wiki.tests import revision, locale
class LocalizationDashTests(TestCase):
@@ -26,6 +27,45 @@ def test_redirect_to_contributor_dash(self):
locale='en-US'))
+def LocalizationDashAnnouncementsTests(TestCase):
+
+ def setUp(self):
+ self.locale1 = locale(save=True, locale='es')
+
+ self.u1 = user(save=True)
+ self.u2 = user(save=True)
+ self.u3 = user(save=True)
+
+ self.u1.is_superuser = 1
+ self.u1.save()
+
+ self.locale1.leaders.add(self.u2)
+ self.locale1.save()
+
+ self.announcement = announcement(save=True, creator=self.u2,
+ locale=self.locale1, content="Look at me!",
+ show_after=datetime(2012, 01, 01, 0, 0, 0))
+
+ def test_show_create(self):
+ self.client.login(username=self.u1.username, password='testpass')
+ resp = self.client.get(reverse('dashboards.localization'))
+ self.assertContains(resp, 'id="create-announcement"')
+
+ def test_show_for_authed(self):
+ self.client.login(username=self.u2.username, password='testpass')
+ resp = self.client.get(reverse('dashboards.localization'))
+ self.assertContains(resp, 'id="create-announcement"')
+
+ def test_hide_for_not_authed(self):
+ self.client.login(username=self.u3.username, password='testpass')
+ resp = self.client.get(reverse('dashboards.localization'))
+ self.assertNotContains(resp, 'id="create-announcement"')
+
+ def test_hide_for_anon(self):
+ resp = self.client.get(reverse('dashboards.localization'))
+ self.assertNotContains(resp, 'id="create-announcement"')
+
+
class ContributorDashTests(TestCase):
def test_main_view(self):
"""Assert the top page of the contributor dash resolves, renders."""
View
2 apps/dashboards/utils.py
@@ -5,6 +5,7 @@
import jingo
from announcements.models import Announcement
+from announcements.forms import AnnouncementForm
from dashboards import ACTIONS_PER_PAGE
from sumo_locales import LOCALES
from sumo.utils import paginate
@@ -55,6 +56,7 @@ def render_readouts(request, readouts, template, locale=None, extra_data=None):
'is_watching_default_ready':
ReadyRevisionEvent.is_notifying(request.user),
'on_default_locale': on_default_locale,
+ 'announce_form': AnnouncementForm(),
'announcements': Announcement.get_for_locale_name(current_locale),
}
if extra_data:
View
14 apps/dashboards/views.py
@@ -1,5 +1,4 @@
import colorsys
-from functools import partial
import json
import logging
import math
@@ -13,6 +12,7 @@
from tower import ugettext as _
from access.decorators import login_required
+from announcements.views import user_can_announce
from announcements.models import Announcement
from dashboards.personal import GROUP_DASHBOARDS
from dashboards.readouts import (overview_rows, READOUTS, L10N_READOUTS,
@@ -23,6 +23,7 @@
from sumo.redis_utils import redis_client, RedisError
from sumo.urlresolvers import reverse
from sumo.utils import paginate, smart_int
+from wiki.models import Locale
log = logging.getLogger('k.dashboards')
@@ -69,7 +70,16 @@ def localization(request):
"""Render aggregate data about articles in a non-default locale."""
if request.locale == settings.WIKI_DEFAULT_LANGUAGE:
return HttpResponseRedirect(reverse('dashboards.contributors'))
- data = {'overview_rows': overview_rows(request.locale)}
+ locales = Locale.objects.filter(locale=request.locale)
+ if locales:
+ permission = user_can_announce(request.user, locales[0])
+ else:
+ permission = False
+
+ data = {
+ 'overview_rows': overview_rows(request.locale),
+ 'user_can_announce': permission
+ }
return render_readouts(request, L10N_READOUTS, 'localization.html',
extra_data=data)
View
64 media/js/dashboards.js
@@ -3,6 +3,7 @@
initReadoutModes();
initWatchMenu();
initNeedsChange();
+ initAnnouncements();
}
// Hook up readout mode links (like "This Week" and "All Time") to swap
@@ -78,7 +79,68 @@
if(!$(e.target).is('a')) {
$(this).toggleClass('active');
}
- })
+ });
+ }
+
+ function initAnnouncements() {
+ var $form = $('#create-announcement form');
+ $form.find('button.btn-submit').on('click', function(ev) {
+ ev.preventDefault();
+
+ var $kbox = $('#create-announcement .kbox');
+
+ $form.addClass('wait');
+ $form.find('.error').remove();
+
+ $.ajax({
+ type: 'POST',
+ url: $form.prop('action'),
+ data: $form.serialize(),
+ statusCode: {
+ 200: function(data) {
+ $form.removeClass('wait');
+ $kbox.hide(200, function() {
+ $kbox.data('kbox').close();
+ $kbox.show();
+ });
+ var $success = $('#create-announcement').children('.success')
+ .show().css({ opacity: 1});
+
+ setTimeout(function() {
+ $success.animate({opacity: 0}, 1000);
+ }, 4000);
+ },
+ 400: function(jxr) {
+ var data, field;
+ $form.removeClass('wait');
+ try {
+ data = JSON.parse(jxr.responseText);
+ } catch(e) {
+ data = {};
+ }
+ for (field in data) {
+ $form.find('[name=' + field + ']').parent()
+ .after('<li class="error">' + data[field] + '</li>');
+ }
+ }
+ }
+ });
+ });
+
+ $('.announcements li a.delete').on('click', function(ev) {
+ ev.preventDefault();
+ var $this = $(this);
+ $.ajax({
+ type: 'POST',
+ url: $this.prop('href'),
+ data: {
+ 'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
+ },
+ success: function() {
+ $this.closest('li').remove();
+ }
+ });
+ });
}
$(document).ready(init);
View
80 media/less/kbdashboards.less
@@ -480,3 +480,83 @@
}
}
}
+
+#create-announcement {
+ .btn {
+ img {
+ background: transparent url("../img/icons-sprite.png") no-repeat -50px -978px;
+ height: 14px;
+ width: 14px;
+ display: inline-block;
+ position: relative;
+ left: -2px;
+ top: 2px;
+ }
+ }
+
+ .success {
+ display: none;
+ padding: 3px;
+ border-radius: 5px;
+ background: #c1e58f;
+ }
+
+ .kbox-container {
+ position: relative;
+ top: 4px;
+
+ &:before {
+ background: #fff;
+ content: '';
+ display: block;
+ height: 15px;
+ left: 58px;
+ margin-bottom: -13px;
+ position: relative;
+ top: -8px;
+ .box-shadow(-1px -1px 1px rgba(0, 0, 0, 0.2));
+ .transform(rotate(45deg));
+ width: 15px;
+ z-index: 1;
+ }
+
+ form {
+ ul {
+ padding: 0;
+ li {
+ list-style: none;
+ margin-bottom: 10px;
+
+ label {
+ width: 100px;
+ text-align: right;
+ display: inline-block;
+ margin-right: 5px;
+ }
+
+ textarea {
+ width: 335px;
+ }
+ }
+ }
+
+ .btn-submit {
+ margin-left: 105px;
+ }
+
+ .error {
+ color: #f00;
+ margin-bottom: 25px;
+ }
+
+ .spinner {
+ opacity: 0;
+ vertical-align: bottom;
+ transition: opacity 0.1s;
+ }
+ &.wait .spinner {
+ opacity: 1;
+ }
+ }
+ }
+}
View
1 urls.py
@@ -31,6 +31,7 @@
(r'^kpi/', include('kpi.urls')),
(r'^products', include('products.urls')),
(r'^topics', include('topics.urls')),
+ (r'^announcements', include('announcements.urls')),
# Kitsune admin (not Django admin).
(r'^admin/', include(admin.site.urls)),

0 comments on commit f67ea61

Please sign in to comment.
Something went wrong with that request. Please try again.