User notifications
This commit is contained in:
Родитель
312bccaceb
Коммит
7872975fbf
|
@ -14,8 +14,10 @@ from tower import ugettext as _, ugettext_lazy as _lazy
|
|||
|
||||
import amo
|
||||
from amo.utils import slug_validator
|
||||
from .models import (UserProfile, BlacklistedUsername, BlacklistedEmailDomain,
|
||||
BlacklistedPassword, DjangoUser)
|
||||
from .models import (UserProfile, UserNotification, BlacklistedUsername,
|
||||
BlacklistedEmailDomain, BlacklistedPassword, DjangoUser)
|
||||
from .widgets import NotificationsSelectMultiple
|
||||
import users.notifications as email
|
||||
from . import tasks
|
||||
|
||||
log = commonware.log.getLogger('z.users')
|
||||
|
@ -213,10 +215,32 @@ class UserEditForm(UserRegisterForm, PasswordMixin):
|
|||
|
||||
photo = forms.FileField(label=_lazy(u'Profile Photo'), required=False)
|
||||
|
||||
notifications = forms.MultipleChoiceField(
|
||||
choices=email.NOTIFICATIONS_CHOICES,
|
||||
widget=NotificationsSelectMultiple,
|
||||
initial=email.NOTIFICATIONS_DEFAULT,
|
||||
required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request', None)
|
||||
super(UserEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance:
|
||||
default = dict((i, n.default_checked) for i, n
|
||||
in email.NOTIFICATIONS_BY_ID.items())
|
||||
user = dict((n.notification_id, n.enabled) for n
|
||||
in self.instance.notifications.all())
|
||||
default.update(user)
|
||||
|
||||
self.fields['notifications'].initial = [i for i, v
|
||||
in default.items() if v]
|
||||
|
||||
# Mark unsaved options as "new"
|
||||
user = (self.instance.notifications
|
||||
.values_list('notification_id', flat=True))
|
||||
for c in self.fields['notifications'].choices:
|
||||
c[1].new = c[0] not in user
|
||||
|
||||
# TODO: We should inherit from a base form not UserRegisterForm
|
||||
if self.fields.get('recaptcha'):
|
||||
del self.fields['recaptcha']
|
||||
|
@ -284,6 +308,11 @@ class UserEditForm(UserRegisterForm, PasswordMixin):
|
|||
amo.log(amo.LOG.CHANGE_PASSWORD)
|
||||
log.info(u'User (%s) changed their password' % u)
|
||||
|
||||
for (i, n) in email.NOTIFICATIONS_BY_ID.items():
|
||||
enabled = n.mandatory or (str(i) in data['notifications'])
|
||||
UserNotification.update_or_create(user=u, notification_id=i,
|
||||
update={'enabled': enabled})
|
||||
|
||||
log.debug(u'User (%s) updated their profile' % u)
|
||||
|
||||
u.save()
|
||||
|
|
|
@ -317,6 +317,22 @@ class UserProfile(amo.models.ModelBase):
|
|||
return c
|
||||
|
||||
|
||||
class UserNotification(amo.models.ModelBase):
|
||||
user = models.ForeignKey(UserProfile, related_name='notifications')
|
||||
notification_id = models.IntegerField()
|
||||
enabled = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
db_table = 'users_notifications'
|
||||
|
||||
@staticmethod
|
||||
def update_or_create(update={}, **kwargs):
|
||||
rows = UserNotification.objects.filter(**kwargs).update(**update)
|
||||
if not rows:
|
||||
update.update(dict(**kwargs))
|
||||
obj = UserNotification.objects.create(**update)
|
||||
|
||||
|
||||
class RequestUserManager(amo.models.ManagerBase):
|
||||
|
||||
def get_query_set(self):
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
from inspect import isclass
|
||||
|
||||
from celery.datastructures import AttributeDict
|
||||
from tower import ugettext_lazy as _
|
||||
|
||||
|
||||
class _NOTIFICATION(object):
|
||||
pass
|
||||
|
||||
|
||||
class thanks(_NOTIFICATION):
|
||||
id = 2
|
||||
group = 'user'
|
||||
short = 'dev_thanks'
|
||||
label = _('an add-on developer thanks me for a contribution')
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class reply(_NOTIFICATION):
|
||||
id = 3
|
||||
group = 'user'
|
||||
short = 'reply'
|
||||
label = _('an add-on developer replies to my review')
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class new_features(_NOTIFICATION):
|
||||
id = 4
|
||||
group = 'user'
|
||||
short = 'new_features'
|
||||
label = _('new add-ons or Firefox features are available')
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class upgrade_success(_NOTIFICATION):
|
||||
id = 5
|
||||
group = 'dev'
|
||||
short = 'upgrade_success'
|
||||
label = _("my add-on's compatibility is upgraded successfully")
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class sdk_upgrade_success(_NOTIFICATION):
|
||||
id = 6
|
||||
group = 'dev'
|
||||
short = 'sdk_upgrade_success'
|
||||
label = _("my sdk-based add-on is upgraded successfully")
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class new_review(_NOTIFICATION):
|
||||
id = 7
|
||||
group = 'dev'
|
||||
short = 'new_review'
|
||||
label = _("someone writes a review of my add-on")
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class announcements(_NOTIFICATION):
|
||||
id = 8
|
||||
group = 'dev'
|
||||
short = 'announcements'
|
||||
label = _("add-on contests or events are announced")
|
||||
mandatory = False
|
||||
default_checked = True
|
||||
|
||||
|
||||
class upgrade_fail(_NOTIFICATION):
|
||||
id = 9
|
||||
group = 'dev'
|
||||
short = 'upgrade_fail'
|
||||
label = _("my add-on's compatibility cannot be upgraded")
|
||||
mandatory = True
|
||||
default_checked = True
|
||||
|
||||
|
||||
class sdk_upgrade_fail(_NOTIFICATION):
|
||||
id = 10
|
||||
group = 'dev'
|
||||
short = 'sdk_upgrade_fail'
|
||||
label = _("my sdk-based add-on cannot be ugpraded")
|
||||
mandatory = True
|
||||
default_checked = True
|
||||
|
||||
|
||||
class editor_reviewed(_NOTIFICATION):
|
||||
id = 11
|
||||
group = 'dev'
|
||||
short = 'editor_reviewed'
|
||||
label = _("my add-on is reviewed by an editor")
|
||||
mandatory = True
|
||||
default_checked = True
|
||||
|
||||
|
||||
class individual_contact(_NOTIFICATION):
|
||||
id = 12
|
||||
group = 'dev'
|
||||
short = 'individual_contact'
|
||||
label = _("Mozilla needs to contact me about my individual add-on")
|
||||
mandatory = True
|
||||
default_checked = True
|
||||
|
||||
|
||||
|
||||
NOTIFICATION_GROUPS = {'dev': _('Developer'),
|
||||
'user': _('User Notifications')}
|
||||
|
||||
NOTIFICATIONS = [x for x in vars().values()
|
||||
if isclass(x) and issubclass(x, _NOTIFICATION)
|
||||
and x != _NOTIFICATION]
|
||||
|
||||
NOTIFICATIONS_BY_ID = dict((l.id, l) for l in NOTIFICATIONS)
|
||||
NOTIFICATION = AttributeDict((l.__name__, l) for l in NOTIFICATIONS)
|
||||
NOTIFICATIONS_DEV = [l.id for l in NOTIFICATIONS if l.group == 'dev']
|
||||
NOTIFICATIONS_USER = [l.id for l in NOTIFICATIONS if l.group == 'user']
|
||||
|
||||
NOTIFICATIONS_DEFAULT = [l.id for l in NOTIFICATIONS if l.default_checked]
|
||||
|
||||
NOTIFICATIONS_CHOICES = [(l.id, l.label) for l in NOTIFICATIONS]
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
{% extends "impala/base.html" %}
|
||||
{% from 'includes/forms.html' import required %}
|
||||
{% from 'devhub/includes/macros.html' import some_html_tip %}
|
||||
|
||||
{% block title %}{{ page_title(_('Account Settings')) }}{% endblock %}
|
||||
|
||||
{% block extrahead %}
|
||||
<link rel="stylesheet" href="{{ media('css/zamboni/translations/trans.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="secondary">
|
||||
<h2>{{ _('Add-on Account') }}</h2>
|
||||
<ul>
|
||||
<li><a href="{{ request.user.get_profile().get_url_path() }}">{{ _('View Profile') }}</a></li>
|
||||
<li><a href="{{ url('users.edit') }}">{{ _('Edit Profile') }}</a></li>
|
||||
<li><a href="{{ url('collections.user', amo_user.username) }}">{{ _('My Collections') }}</a></li>
|
||||
<li><a href="{{ url('collections.detail', amo_user.username, 'favorites') }}">{{ _('My Favorites') }}</a></li>
|
||||
</ul>
|
||||
|
||||
<h4>{{ _('Delete Account') }}</h4>
|
||||
<p class="note">
|
||||
{% trans %}
|
||||
We'd hate to see you go! If we're being a bit too noisy, you can
|
||||
always just change <a href="#acct-notify">how often we contact you</a>.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
<p id="acct-delete" class="note">
|
||||
<a href="{{ url('users.delete') }}" class="button scary"
|
||||
title="{{ _('Permanently delete your account') }}">{{ _('Delete Account') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="primary prettyform" role="main">
|
||||
{% include "messages.html" %}
|
||||
<form method="post" action="" class="user-input island"
|
||||
enctype="multipart/form-data">
|
||||
{{ csrf() }}
|
||||
<div id="user-edit" class="tab-wrapper">
|
||||
<div id="user-account" class="tab-panel">
|
||||
<fieldset id="acct-account">
|
||||
<legend>{{ _('My account') }}</legend>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="id_username">{{ _('Username') }} {{ required() }}</label>
|
||||
{{ form.username }}
|
||||
{{ form.username.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_email">{{ _('Email Address') }} {{ required() }}</label>
|
||||
{{ form.email }}
|
||||
{{ form.email.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label> </label>
|
||||
<label for="id_emailhidden" class="check">
|
||||
{{ form.emailhidden }}
|
||||
{{ _('Hide email address from other users') }}
|
||||
</label>
|
||||
{{ form.emailhidden.errors }}
|
||||
</li>
|
||||
</ul>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="acct-password">
|
||||
<legend>{{ _('Password') }}</legend>
|
||||
<ol>
|
||||
<li>
|
||||
<label for="id_oldpassword">{{ _('Old Password') }}</label>
|
||||
{{ form.oldpassword }}
|
||||
{{ form.oldpassword.errors }}
|
||||
</li>
|
||||
<li>
|
||||
{% with form_user=form.instance %}{% include "users/tougher_password.html" %}{% endwith %}
|
||||
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_password">{{ _('New Password') }}</label>
|
||||
{{ form.password }}
|
||||
{{ form.password.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_password2">{{ _('Confirm Password') }}</label>
|
||||
{{ form.password2 }}
|
||||
{{ form.password2.errors }}
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
|
||||
<fieldset id="acct-notify">
|
||||
<legend>{{ _('Notifications') }}</legend>
|
||||
<p>
|
||||
{% trans %}
|
||||
From time to time, Mozilla may send you email about upcoming
|
||||
releases and add-on events. Please select the topics you are
|
||||
interested in below:
|
||||
{% endtrans %}
|
||||
</p>
|
||||
{{ form.notifications }}
|
||||
|
||||
<p class="note">
|
||||
{% trans %}
|
||||
Mozilla reserves the right to contact you individually about
|
||||
specific concerns with your hosted add-ons.
|
||||
{% endtrans %}
|
||||
</p>
|
||||
</fieldset>
|
||||
</div>{# /#user-account #}
|
||||
<div id="user-profile" class="tab-panel">
|
||||
<fieldset id="profile-personal">
|
||||
<legend>{{ _('Profile information') }}</legend>
|
||||
<ol>
|
||||
<li>
|
||||
<label for="id_display_name">{{ _('Display Name') }}</label>
|
||||
{{ form.display_name }}
|
||||
{{ form.display_name.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_location">{{ _('Location') }}</label>
|
||||
{{ form.location }}
|
||||
{{ form.location.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_occupation">{{ _('Occupation') }}</label>
|
||||
{{ form.occupation }}
|
||||
{{ form.occupation.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_homepage">{{ _('Homepage') }}</label>
|
||||
{{ form.homepage }}
|
||||
{{ form.homepage.errors }}
|
||||
</li>
|
||||
<li class="profile-photo">
|
||||
<label for="id_photo">{{ _('Profile Photo') }}</label>
|
||||
<div class="invisible-upload">
|
||||
<a class="button" href="#">Choose Photo…</a>
|
||||
<input type="file" id="id_photo" name="photo">
|
||||
</div>
|
||||
{{ form.photo.errors }}
|
||||
<img src="{{ amouser.picture_url }}"
|
||||
alt="{% if not amouser.picture_type %}{{ _('No Photo') }}{% endif %}"
|
||||
class="avatar photo" />
|
||||
|
||||
{% if amouser.picture_type %}
|
||||
<a href="{{ url('users.delete_photo') }}" class="delete">{{ _('delete current') }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
<fieldset id="profile-detail" class="clearboth">
|
||||
<legend>{{ _('Details') }}</legend>
|
||||
<p>
|
||||
{% trans -%}
|
||||
Introduce yourself to the community, if you like!
|
||||
This text will appear publicly on your user info page.
|
||||
{%- endtrans %}
|
||||
</p>
|
||||
{{ form.bio }}
|
||||
{{ some_html_tip() }}
|
||||
{{ form.bio.errors }}
|
||||
</fieldset> {# /.profile-detail #}
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<label for="id_display_collections" class="check">
|
||||
{{ form.display_collections }}
|
||||
{{ _('Display the collections I have created') }}
|
||||
</label>
|
||||
{{ form.display_collections.errors }}
|
||||
</li>
|
||||
<li>
|
||||
<label for="id_display_collections_fav" class="check">
|
||||
{{ form.display_collections_fav }}
|
||||
{{ _("Display collections I'm following") }}
|
||||
</label>
|
||||
{{ form.display_collections_fav.errors }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>{# /#user-profile #}
|
||||
<div class="listing-footer">
|
||||
<button type="submit" class="button prominent">{{ _('Update') }}</button>
|
||||
</div>
|
||||
</div>{# /.tab-wrapper #}
|
||||
</form>
|
||||
</div>{# /.primary #}
|
||||
{% endblock content %}
|
|
@ -1,3 +1,3 @@
|
|||
{% if form_user.needs_tougher_password -%}
|
||||
<p>{{ _('For your account a password must contain at least 8 characters including letters and numbers.') }}</p>
|
||||
<p class="note">{{ _('For your account a password must contain at least 8 characters including letters and numbers.') }}</p>
|
||||
{%- endif %}
|
||||
|
|
|
@ -16,10 +16,11 @@ from access.models import Group, GroupUser
|
|||
from addons.models import Addon, AddonUser
|
||||
import amo
|
||||
from amo.helpers import urlparams
|
||||
from amo.pyquery_wrapper import PyQuery
|
||||
from amo.pyquery_wrapper import PyQuery as pq
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.tests.test_helpers import AbuseBase
|
||||
from users.models import BlacklistedPassword, UserProfile
|
||||
from users.models import BlacklistedPassword, UserProfile, UserNotification
|
||||
import users.notifications as email
|
||||
from users.utils import EmailResetCode
|
||||
|
||||
|
||||
|
@ -59,6 +60,7 @@ class TestEdit(UserViewBase):
|
|||
self.client.login(username='jbalogh@mozilla.com', password='foo')
|
||||
self.user = UserProfile.objects.get(username='jbalogh')
|
||||
self.url = reverse('users.edit')
|
||||
self.impala_url = reverse('users.edit_impala')
|
||||
self.correct = {'username': 'jbalogh', 'email': 'jbalogh@mozilla.com',
|
||||
'oldpassword': 'foo', 'password': 'longenough',
|
||||
'password2': 'longenough'}
|
||||
|
@ -134,6 +136,20 @@ class TestEdit(UserViewBase):
|
|||
self.assertContains(r, data['bio'])
|
||||
eq_(unicode(self.get_profile().bio), data['bio'])
|
||||
|
||||
def test_edit_notifications(self):
|
||||
post = self.correct.copy()
|
||||
post['notifications'] = [2, 4, 6]
|
||||
|
||||
res = self.client.post(self.impala_url, post)
|
||||
eq_(res.status_code, 302)
|
||||
|
||||
eq_(UserNotification.objects.count(), len(email.NOTIFICATION))
|
||||
eq_(UserNotification.objects.filter(enabled=True).count(), 3)
|
||||
|
||||
res = self.client.get(self.impala_url, post)
|
||||
doc = pq(res.content)
|
||||
eq_(doc('[name=notifications]:checked').length, 3)
|
||||
|
||||
|
||||
class TestPasswordAdmin(UserViewBase):
|
||||
fixtures = ['base/users']
|
||||
|
@ -309,7 +325,7 @@ class TestRegistration(UserViewBase):
|
|||
# User doesn't have a confirmation code
|
||||
url = reverse('users.confirm', args=[self.user.id, 'code'])
|
||||
r = self.client.get(url, follow=True)
|
||||
anon = PyQuery(r.content)('body').attr('data-anonymous')
|
||||
anon = pq(r.content)('body').attr('data-anonymous')
|
||||
self.assertTrue(anon)
|
||||
|
||||
self.user_profile.confirmationcode = "code"
|
||||
|
@ -331,7 +347,7 @@ class TestRegistration(UserViewBase):
|
|||
# User doesn't have a confirmation code
|
||||
url = reverse('users.confirm.resend', args=[self.user.id])
|
||||
r = self.client.get(url, follow=True)
|
||||
anon = PyQuery(r.content)('body').attr('data-anonymous')
|
||||
anon = pq(r.content)('body').attr('data-anonymous')
|
||||
self.assertTrue(anon)
|
||||
|
||||
self.user_profile.confirmationcode = "code"
|
||||
|
@ -355,7 +371,7 @@ class TestProfile(UserViewBase):
|
|||
"""Grab profile, return edit links."""
|
||||
url = reverse('users.profile', args=[id])
|
||||
r = self.client.get(url)
|
||||
return PyQuery(r.content)('p.editprofile a')
|
||||
return pq(r.content)('p.editprofile a')
|
||||
|
||||
# Anonymous user.
|
||||
links = get_links(self.user.id)
|
||||
|
|
|
@ -20,6 +20,10 @@ detail_patterns = patterns('',
|
|||
url('^abuse', views.report_abuse, name='users.abuse'),
|
||||
)
|
||||
|
||||
impala_users_patterns = patterns('',
|
||||
url('^edit$', views.edit_impala, name='users.edit_impala'),
|
||||
)
|
||||
|
||||
users_patterns = patterns('',
|
||||
url('^ajax$', views.ajax, name='users.ajax'),
|
||||
url('^delete$', views.delete, name='users.delete'),
|
||||
|
@ -47,5 +51,6 @@ users_patterns = patterns('',
|
|||
urlpatterns = patterns('',
|
||||
# URLs for a single user.
|
||||
('^user/(?P<user_id>\d+)/', include(detail_patterns)),
|
||||
('^i/users/', include(impala_users_patterns)),
|
||||
('^users/', include(users_patterns)),
|
||||
)
|
||||
|
|
|
@ -127,6 +127,62 @@ def delete_photo(request):
|
|||
return jingo.render(request, 'users/delete_photo.html', dict(user=u))
|
||||
|
||||
|
||||
@write
|
||||
@login_required
|
||||
def edit_impala(request):
|
||||
# Don't use request.amo_user since it has too much caching.
|
||||
amouser = UserProfile.objects.get(pk=request.user.id)
|
||||
if request.method == 'POST':
|
||||
# ModelForm alters the instance you pass in. We need to keep a copy
|
||||
# around in case we need to use it below (to email the user)
|
||||
original_email = amouser.email
|
||||
form = forms.UserEditForm(request.POST, request.FILES, request=request,
|
||||
instance=amouser)
|
||||
if form.is_valid():
|
||||
messages.success(request, _('Profile Updated'))
|
||||
if amouser.email != original_email:
|
||||
l = {'user': amouser,
|
||||
'mail1': original_email,
|
||||
'mail2': amouser.email}
|
||||
log.info(u"User (%(user)s) has requested email change from"
|
||||
"(%(mail1)s) to (%(mail2)s)" % l)
|
||||
messages.info(request, _('Email Confirmation Sent'),
|
||||
_(('An email has been sent to {0} to confirm your new ' +
|
||||
'email address. For the change to take effect, you ' +
|
||||
'need to click on the link provided in this email. ' +
|
||||
'Until then, you can keep logging in with your ' +
|
||||
'current email address.')).format(amouser.email))
|
||||
|
||||
domain = settings.DOMAIN
|
||||
token, hash = EmailResetCode.create(amouser.id, amouser.email)
|
||||
url = "%s%s" % (settings.SITE_URL,
|
||||
reverse('users.emailchange', args=[amouser.id,
|
||||
token, hash]))
|
||||
t = loader.get_template('users/email/emailchange.ltxt')
|
||||
c = {'domain': domain, 'url': url, }
|
||||
send_mail(_(("Please confirm your email address "
|
||||
"change at %s") % domain),
|
||||
t.render(Context(c)), None, [amouser.email],
|
||||
use_blacklist=False)
|
||||
|
||||
# Reset the original email back. We aren't changing their
|
||||
# address until they confirm the new one
|
||||
amouser.email = original_email
|
||||
form.save()
|
||||
return http.HttpResponseRedirect(reverse('users.edit_impala'))
|
||||
else:
|
||||
|
||||
messages.error(request, _('Errors Found'),
|
||||
_('There were errors in the changes '
|
||||
'you made. Please correct them and '
|
||||
'resubmit.'))
|
||||
else:
|
||||
form = forms.UserEditForm(instance=amouser)
|
||||
|
||||
return jingo.render(request, 'users/edit_impala.html',
|
||||
{'form': form, 'amouser': amouser})
|
||||
|
||||
|
||||
@write
|
||||
@login_required
|
||||
def edit(request):
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.encoding import force_unicode
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from tower import ugettext as _
|
||||
|
||||
import users.notifications as email
|
||||
|
||||
|
||||
class NotificationsSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
"""Widget that formats the notification checkboxes."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(self.__class__, self).__init__(**kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
str_values = set([int(v) for v in value]) or []
|
||||
final_attrs = self.build_attrs(attrs, name=name)
|
||||
groups = {}
|
||||
|
||||
for c in sorted(self.choices):
|
||||
notification = email.NOTIFICATIONS_BY_ID[c[0]]
|
||||
cb_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], c[0]))
|
||||
notes = []
|
||||
|
||||
if notification.mandatory:
|
||||
cb_attrs = dict(cb_attrs, disabled=1)
|
||||
notes.append(u'<abbr title="required" class="req">*</abbr>')
|
||||
|
||||
if c[1].new:
|
||||
notes.append(u'<sup class="msg">%s</sup>' % _('new'))
|
||||
|
||||
cb = forms.CheckboxInput(
|
||||
cb_attrs, check_test=lambda value: value in str_values)
|
||||
|
||||
rendered_cb = cb.render(name, c[0])
|
||||
label_for = u' for="%s"' % cb_attrs['id']
|
||||
|
||||
groups.setdefault(notification.group, []).append(
|
||||
u'<li><label class="check" %s>%s %s %s</label></li>' % (
|
||||
label_for, rendered_cb, c[1], ''.join(notes)
|
||||
))
|
||||
|
||||
output = []
|
||||
template = u'<li><label>%s</label><ul class="checkboxes">%s</ul></li>'
|
||||
for e, name in email.NOTIFICATION_GROUPS.items():
|
||||
if e in groups:
|
||||
output.append(template % (name, u'\n'.join(groups[e])))
|
||||
|
||||
return mark_safe(u'<ol class="complex">%s</ul>' % u'\n'.join(output))
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
.sprite-pos(7, 64, 3px);
|
||||
}
|
||||
}
|
||||
&.developer { // Red
|
||||
&.developer, &.scary { // Red
|
||||
background: #bc2b1a;
|
||||
.gradient-two-color(#f84b4e, #bc2b1a);
|
||||
color: #fff;
|
||||
|
@ -175,3 +175,45 @@
|
|||
.thunderbird .listing-grid .install-shell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Invisible Upload */
|
||||
.invisible-upload {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
input {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
height: 110%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
/* stack this below the navbar: */
|
||||
z-index: 0;
|
||||
outline: none;
|
||||
}
|
||||
&:hover input {
|
||||
font-size: 1000px;
|
||||
}
|
||||
&:hover, &:focused {
|
||||
a.button {
|
||||
border-color: #25f;
|
||||
.box-shadow(inset 0 0 2px #fff);
|
||||
}
|
||||
a.link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
a.button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
a.link {
|
||||
text-decoration: none;
|
||||
color: #0055EE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,3 +54,107 @@ form p {
|
|||
.edit_initially_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prettyform {
|
||||
fieldset {
|
||||
border-bottom: 1px dotted #C9DDF2;
|
||||
margin: 0 0 1em;
|
||||
padding: 1em;
|
||||
legend {
|
||||
margin-left: 2em;
|
||||
color: #D0D9EB;
|
||||
float: right;
|
||||
font-size: 25px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
label {
|
||||
color: #555;
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
padding-bottom: 1em;
|
||||
text-transform: lowercase;
|
||||
width: 120px;
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
&.check {
|
||||
width: auto;
|
||||
text-align: left;
|
||||
color: #777;
|
||||
&:hover {
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
.req {
|
||||
color: @red;
|
||||
}
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
input[type="text"], input[type="password"], select, textarea {
|
||||
border: 1px solid #B2C8E0;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
box-shadow: 2px 2px #EFF6FE inset;
|
||||
color: #5B738E;
|
||||
font-size: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding: 4px 7px;
|
||||
&:focus {
|
||||
color: #394D63;
|
||||
border-color: #809CBA;
|
||||
}
|
||||
}
|
||||
p.note {
|
||||
color: #888888;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-top: 0;
|
||||
padding-left: 138px;
|
||||
clear: both;
|
||||
width: 300px;
|
||||
}
|
||||
sup {
|
||||
bottom: 4px;
|
||||
font-size: 0.7em;
|
||||
position: relative;
|
||||
&.msg {
|
||||
color: @red;
|
||||
}
|
||||
}
|
||||
legend + p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.checkboxes {
|
||||
display: inline-block;
|
||||
padding-bottom: 1em;
|
||||
label {
|
||||
padding: 0 0 4px;
|
||||
width: auto;
|
||||
text-align: left;
|
||||
input {
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.complex {
|
||||
label, ul {
|
||||
float: left;
|
||||
}
|
||||
label {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
&.delete {
|
||||
color: @red;
|
||||
padding-left: 1em;
|
||||
}
|
||||
}
|
||||
.listing-footer {
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.trans {
|
||||
[lang] {
|
||||
display: none;
|
||||
&:first-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.init-trans {
|
||||
display: none;
|
||||
}
|
||||
.cloned {
|
||||
color: #ccc;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
|
@ -175,6 +175,11 @@ a {
|
|||
}
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
&.note {
|
||||
color: #666;
|
||||
line-height: 1.2em;
|
||||
padding: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
section section {
|
||||
margin: 0;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
DROP TABLE IF EXISTS `users_notifications`;
|
||||
CREATE TABLE `users_notifications` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`notification_id` int(11) NOT NULL,
|
||||
`created` datetime DEFAULT NULL,
|
||||
`modified` datetime DEFAULT NULL,
|
||||
`enabled` tinyint(1) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
ALTER TABLE `users_notifications`
|
||||
ADD CONSTRAINT FOREIGN KEY (`user_id`) REFERENCES `users` (`id`);
|
|
@ -425,6 +425,7 @@ MINIFY_BUNDLES = {
|
|||
'css/impala/addon_details.less',
|
||||
'css/impala/expando.less',
|
||||
'css/impala/popups.less',
|
||||
'css/impala/l10n.less',
|
||||
'css/impala/contributions.less',
|
||||
'css/impala/lightbox.less',
|
||||
'css/impala/copy.less',
|
||||
|
|
Загрузка…
Ссылка в новой задаче