Bug 1041873: Add mass deletion tool.
This commit is contained in:
Родитель
cd5b1aba9c
Коммит
70ab314f14
|
@ -488,19 +488,19 @@ class OBJECT_DELETED(_LOG):
|
|||
|
||||
class ADMIN_USER_EDITED(_LOG):
|
||||
id = 103
|
||||
format = _(u'User {user} edited, reason: {1}')
|
||||
format = _(u'User {0} edited, reason: {1}')
|
||||
admin_event = True
|
||||
|
||||
|
||||
class ADMIN_USER_ANONYMIZED(_LOG):
|
||||
id = 104
|
||||
format = _(u'User {user} anonymized.')
|
||||
format = _(u'User {0} anonymized.')
|
||||
admin_event = True
|
||||
|
||||
|
||||
class ADMIN_USER_RESTRICTED(_LOG):
|
||||
id = 105
|
||||
format = _(u'User {user} restricted.')
|
||||
format = _(u'User {0} restricted.')
|
||||
admin_event = True
|
||||
|
||||
|
||||
|
@ -522,6 +522,12 @@ class THEME_REVIEW(_LOG):
|
|||
format = _(u'{addon} reviewed.')
|
||||
|
||||
|
||||
class ADMIN_MASS_DELETE(_LOG):
|
||||
id = 109
|
||||
format = _(u'{0} objects {1} mass deleted, reason: {2}')
|
||||
admin_event = True
|
||||
|
||||
|
||||
class GROUP_USER_ADDED(_LOG):
|
||||
id = 120
|
||||
action_class = 'access'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
@ -58,6 +59,14 @@ class Review(amo.models.ModelBase):
|
|||
db_table = 'reviews'
|
||||
ordering = ('-created',)
|
||||
|
||||
def __unicode__(self):
|
||||
quote = lambda o: "'%s'" % re.sub(r"[\\']", lambda m: '\\' + m.group(0),
|
||||
unicode(o))
|
||||
return u'Review %d: <%s, %s, %s>' % (self.pk,
|
||||
quote(self.addon),
|
||||
quote(self.user),
|
||||
quote(self.title))
|
||||
|
||||
def get_url_path(self):
|
||||
if 'mkt.ratings' in settings.INSTALLED_APPS:
|
||||
return '/app/%s/ratings/%s' % (self.addon.app_slug, self.id)
|
||||
|
|
|
@ -25,6 +25,8 @@ from compat.forms import CompatForm as BaseCompatForm
|
|||
from files.models import File
|
||||
from zadmin.models import SiteEvent, ValidationJob
|
||||
|
||||
from .helpers import MassDeleteHelper
|
||||
|
||||
LOGGER_NAME = 'z.zadmin'
|
||||
log = commonware.log.getLogger(LOGGER_NAME)
|
||||
|
||||
|
@ -131,6 +133,38 @@ class NotifyForm(happyforms.Form):
|
|||
return self.check_template(self.cleaned_data['subject'])
|
||||
|
||||
|
||||
class MassDeleteForm(happyforms.Form):
|
||||
urls = forms.CharField(label=_lazy(u'URLs to delete'),
|
||||
widget=forms.Textarea, required=True)
|
||||
|
||||
reason = forms.CharField(label=_lazy(u'Reason for deletion'),
|
||||
required=True)
|
||||
|
||||
|
||||
class MassDeleteConfirmForm(happyforms.Form):
|
||||
objects = forms.CharField(widget=forms.HiddenInput, required=True)
|
||||
reason = forms.CharField(widget=forms.HiddenInput, required=True)
|
||||
|
||||
def clean_objects(self):
|
||||
try:
|
||||
data = json.loads(self.cleaned_data.get('objects'))
|
||||
|
||||
assert all(all(isinstance(id_, int) for id_ in ids)
|
||||
for ids in data.values())
|
||||
|
||||
for m, ids in data.items():
|
||||
model = MassDeleteHelper.MODEL_MAP[m]
|
||||
objs = model.objects.in_bulk(ids)
|
||||
assert len(objs) == len(ids)
|
||||
|
||||
data[m] = map(objs.get, ids)
|
||||
except Exception, e:
|
||||
raise forms.ValidationError(
|
||||
u'Invalid objects JSON')
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class FeaturedCollectionForm(happyforms.ModelForm):
|
||||
LOCALES = (('', u'(Default Locale)'),) + tuple(
|
||||
(i, product_details.languages[i]['native'])
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
from collections import defaultdict
|
||||
import json
|
||||
import re
|
||||
|
||||
from jingo import register
|
||||
|
||||
from amo.urlresolvers import reverse
|
||||
|
||||
import amo
|
||||
from amo.urlresolvers import resolve, reverse
|
||||
from bandwagon.models import (Collection, CollectionAddon, CollectionUser,
|
||||
CollectionVote, CollectionWatcher)
|
||||
from addons.models import Addon, AddonUser
|
||||
from abuse.models import AbuseReport
|
||||
from files.models import File, FileUpload
|
||||
from reviews.models import Review
|
||||
from users.models import UserProfile
|
||||
from versions.models import Version
|
||||
|
||||
@register.function
|
||||
def admin_site_links():
|
||||
|
@ -39,3 +51,178 @@ def admin_site_links():
|
|||
('Site Status', reverse('amo.monitor')),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class MassDeleteHelper(object):
|
||||
VIEW_MAP = {
|
||||
'addons.': ('addon_id', Addon, 'slug'),
|
||||
'editors.review': ('addon_id', Addon, 'slug'),
|
||||
'zadmin.addon_manage': ('addon_id', Addon, 'slug'),
|
||||
|
||||
'addons.reviews.': ('review_id', Review, 'pk'),
|
||||
|
||||
'users.': ('user_id', UserProfile, 'username'),
|
||||
'users.admin_edit': ('user_id', UserProfile, 'pk'),
|
||||
|
||||
'collections.': (('username', 'slug'),
|
||||
Collection,
|
||||
('author__username', 'slug')),
|
||||
}
|
||||
|
||||
MODEL_MAP = {
|
||||
'Addon': Addon,
|
||||
'Collection': Collection,
|
||||
'Review': Review,
|
||||
'UserProfile': UserProfile,
|
||||
}
|
||||
|
||||
DELETION_MAP = {
|
||||
Addon: {
|
||||
'RELATED_MODELS': (
|
||||
(AbuseReport, 'addon_id'),
|
||||
(AddonUser, 'addon_id'),
|
||||
(CollectionAddon, 'addon_id'),
|
||||
(Review, 'addon_id'),
|
||||
(File, 'version__addon_id'),
|
||||
(Version, 'addon_id'),
|
||||
)
|
||||
},
|
||||
|
||||
UserProfile: {
|
||||
'RELATED_MODELS': (
|
||||
(CollectionWatcher, 'collection__author_id'),
|
||||
(CollectionVote, 'collection__author_id'),
|
||||
(CollectionVote, 'user_id'),
|
||||
(CollectionUser, 'user_id'),
|
||||
(Collection, 'author_id'),
|
||||
(FileUpload, 'user_id'),
|
||||
(AbuseReport, 'reporter_id'),
|
||||
|
||||
(AddonUser, 'user_id'),
|
||||
# Argh. Sometimes I hate Django's ORM.
|
||||
# I want `.exclude(authors__id__not_in=ids)`, but even with
|
||||
# `~Q(...)`, the NOT winds up outside of the generated
|
||||
# subselect rather than in it.
|
||||
(Addon, lambda objects, ids: (
|
||||
objects.filter(authors__id__in=ids)
|
||||
.extra(
|
||||
where=['''
|
||||
`addons`.`id` NOT IN
|
||||
(SELECT `a2`.`id`
|
||||
FROM `addons` AS `a2`
|
||||
INNER JOIN `addons_users` AS `au2`
|
||||
ON `au2`.`addon_id` = `a2`.`id`
|
||||
WHERE `au2`.`user_id` NOT IN %s)
|
||||
'''],
|
||||
params=[ids]))),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
URL_LOP_RE = re.compile(r'^https?://.*?/')
|
||||
VIEW_LOP_RE = re.compile(r'[^.]+\.?$')
|
||||
|
||||
def __init__(self, objects=None, urls=None, reason=None):
|
||||
self.reason = reason
|
||||
|
||||
if objects:
|
||||
self.object_types = objects
|
||||
|
||||
if urls:
|
||||
self.objects = map(self.url_to_object, urls)
|
||||
self.unknown_urls = tuple(l
|
||||
for i, l in enumerate(urls)
|
||||
if not self.objects[i])
|
||||
|
||||
object_types = defaultdict(list)
|
||||
for o in self.objects:
|
||||
if o:
|
||||
object_types[o.__class__.__name__].append(o)
|
||||
|
||||
self.object_types = dict(object_types.iteritems())
|
||||
|
||||
@property
|
||||
def object_types_json(self):
|
||||
return json.dumps(self.object_types,
|
||||
default=lambda o: o.pk)
|
||||
|
||||
|
||||
def url_to_object(self, url):
|
||||
to_tuple = lambda v: v if isinstance(v, tuple) else (v,)
|
||||
|
||||
# Crudely lop off host and protocol part.
|
||||
url = self.URL_LOP_RE.sub('/', url)
|
||||
|
||||
try:
|
||||
r = resolve(url)
|
||||
except:
|
||||
return None
|
||||
|
||||
key = None
|
||||
view_id = r.url_name
|
||||
while not key and view_id:
|
||||
key = self.VIEW_MAP.get(view_id)
|
||||
args = key and map(r.kwargs.get, to_tuple(key[0]))
|
||||
|
||||
if not (key and all(args)):
|
||||
key = None
|
||||
view_id = self.VIEW_LOP_RE.sub('', view_id)
|
||||
|
||||
_, model, fields = key
|
||||
try:
|
||||
return model.objects.get(**dict(zip(to_tuple(fields),
|
||||
args)))
|
||||
except (ValueError, model.DoesNotExist):
|
||||
return None
|
||||
|
||||
def delete_objects(self):
|
||||
for model_name, objs in self.object_types.iteritems():
|
||||
model = self.MODEL_MAP[model_name]
|
||||
ids = [o.pk for o in objs]
|
||||
|
||||
amo.log(amo.LOG.ADMIN_MASS_DELETE, model_name,
|
||||
json.dumps(map(unicode, objs)),
|
||||
self.reason)
|
||||
|
||||
for model_, qs in self.get_related_objects(model, ids):
|
||||
qs.delete()
|
||||
|
||||
qs = model.objects.filter(pk__in=ids)
|
||||
qs.delete()
|
||||
|
||||
def count_related(self, obj):
|
||||
model = obj.__class__
|
||||
counts = defaultdict(lambda: 0)
|
||||
|
||||
for model, qs in self.get_related_objects(model, (obj.pk,)):
|
||||
count = qs.count()
|
||||
if count:
|
||||
counts[model.__name__] += count
|
||||
|
||||
return sorted(counts.items())
|
||||
|
||||
def get_related_objects(self, model, ids, seen=()):
|
||||
if model not in self.DELETION_MAP:
|
||||
return
|
||||
|
||||
seen = set(seen)
|
||||
for key in self.DELETION_MAP[model]['RELATED_MODELS']:
|
||||
model, field = key
|
||||
|
||||
qs = self.get_objects(model, field, ids)
|
||||
if model in self.DELETION_MAP and key not in seen:
|
||||
seen.add(key)
|
||||
|
||||
pks = qs.values_list('pk', flat=True)
|
||||
for model_, qs_ in self.get_related_objects(model, pks,
|
||||
seen=seen):
|
||||
yield model_, qs_
|
||||
|
||||
yield model, qs
|
||||
|
||||
def get_objects(self, model, field, ids):
|
||||
if callable(field):
|
||||
return field(model.objects, ids)
|
||||
else:
|
||||
return model.objects.filter(**{'%s__in' % field: ids})
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}{{ page_title("Spammers! Pr0nmongers! Infringers! Down with them all!") }}{% endblock %}
|
||||
|
||||
{% block bodyclass %}proper-lists admin-mass-delete{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1><acronym title="We do what we must, because we can">{{ _("Mass Deletion Completed") }}</acronym></h1>
|
||||
|
||||
<center>
|
||||
<div>
|
||||
<img src="{{ MEDIA_URL }}img/zamboni/admin/cheers.jpg"/>
|
||||
</div>
|
||||
<caption>
|
||||
{{ _("We have done a great thing today.") }}
|
||||
</caption>
|
||||
</center>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,64 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}{{ page_title("Spammers! Pr0nmongers! Infringers! Down with them all!") }}{% endblock %}
|
||||
|
||||
{% block bodyclass %}proper-lists admin-mass-delete{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1><acronym title="We do what we must, because we can">{{ _("Confirm Mass Deletion") }}</acronym></h1>
|
||||
|
||||
{% if unknown_urls %}
|
||||
<h3>{{ _("Uknown URLs:") }}</h3>
|
||||
<p>{{ _("The following URLs could not be mapped to objects:") }}</p>
|
||||
<ul id="unknown-urls">
|
||||
{% for url in unknown_urls %}
|
||||
<li>{{ url }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<h3>{{ _("The following objects and dependent objects will be deleted:") }}</h3>
|
||||
|
||||
{% for type, objects in deletion_helper.object_types.iteritems() %}
|
||||
<h4>{{ _('%s objects:')|format(type) }}</h4>
|
||||
<ul id="delete-confirm-{{ type }}">
|
||||
{% for object in objects %}
|
||||
<li id="delete-confirm-{{ type }}-{{ object.pk }}"><a href="{{ object.get_url_path() }}">{{ object|string }}</a>
|
||||
<ul>
|
||||
{% for type_, count in deletion_helper.count_related(object) %}
|
||||
<li id="delete-confirm-{{ type }}-{{ object.pk }}-{{ type_ }}">
|
||||
{{ _('%(count)d %(type)s objects')|format(count=count, type=type_) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<center>
|
||||
<h2>{{ _("Caution!") }}</h2>
|
||||
|
||||
<div>
|
||||
<img src="{{ MEDIA_URL }}img/zamboni/admin/warning-you.jpg"/>
|
||||
</div>
|
||||
<caption>
|
||||
{% trans %}
|
||||
<p>
|
||||
With great power comes great <em>don't screw around with this</em>.
|
||||
This action is irreversible. Please do not take it lightly! Please
|
||||
triple-check the above object list.
|
||||
<p>
|
||||
<p><em>I'm watching you.</em></p>
|
||||
{% endtrans %}
|
||||
</caption>
|
||||
</center>
|
||||
|
||||
<form method="post" action="{{ url('zadmin.mass_delete.confirm') }}">
|
||||
{{ csrf() }}
|
||||
{{ confirm_form.as_p() }}
|
||||
<button type="submit">Kill them with <em>napalm</em> fire!</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}{{ page_title("Spammers! Pr0nmongers! Infringers! Down with them all!") }}{% endblock %}
|
||||
|
||||
{% block bodyclass %}proper-lists admin-mass-delete{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1><acronym title="We do what we must, because we can">{{ _("Mass Deletion") }}</acronym></h1>
|
||||
|
||||
<form method="post" action="">
|
||||
{{ csrf() }}
|
||||
{{ mass_delete_form.as_p() }}
|
||||
<button type="submit">Kill them with fire!</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,144 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from mock import Mock, patch
|
||||
from nose.tools import eq_
|
||||
|
||||
import amo.tests
|
||||
from amo.helpers import absolutify
|
||||
from amo.urlresolvers import resolve, reverse
|
||||
from bandwagon.models import (Collection, CollectionAddon, CollectionUser,
|
||||
CollectionVote, CollectionWatcher)
|
||||
from addons.models import Addon, AddonUser
|
||||
from abuse.models import AbuseReport
|
||||
from files.models import File, FileUpload
|
||||
from reviews.models import Review
|
||||
from users.models import UserProfile
|
||||
from versions.models import Version
|
||||
from zadmin.helpers import MassDeleteHelper
|
||||
|
||||
from zadmin.helpers import MassDeleteHelper
|
||||
|
||||
class MassDeletionTest(amo.tests.TestCase):
|
||||
fixtures = ['addons/featured',
|
||||
'base/featured',
|
||||
'base/collections',
|
||||
'base/users',
|
||||
'base/addon_3615',
|
||||
'base/addon_3723_listed',
|
||||
'reviews/test_models',
|
||||
'bandwagon/featured_collections']
|
||||
|
||||
OBJECTS = (
|
||||
{'model': UserProfile, 'pk': 999, 'related': {}},
|
||||
{
|
||||
'model': UserProfile,
|
||||
'pk': 10482,
|
||||
'related': {
|
||||
Collection: [80, 56445, 56446, 56447],
|
||||
}
|
||||
},
|
||||
{'model': Collection, 'pk': 80, 'related': {}},
|
||||
{'model': Collection, 'pk': 56445, 'related': {}},
|
||||
{
|
||||
'model': Addon,
|
||||
'pk': 4,
|
||||
'related': {
|
||||
Review: [1, 2],
|
||||
File: [592],
|
||||
Version: [5, 592],
|
||||
}
|
||||
},
|
||||
{
|
||||
'model': Addon,
|
||||
'pk': 3615,
|
||||
'related': {
|
||||
AddonUser: [2818],
|
||||
CollectionAddon: [207981],
|
||||
File: [67442],
|
||||
Version: [81551],
|
||||
}
|
||||
},
|
||||
{
|
||||
'model': Addon,
|
||||
'pk': 3723,
|
||||
'related': {
|
||||
AddonUser: [2905],
|
||||
Version: [89774],
|
||||
}
|
||||
},
|
||||
{'model': Review, 'pk': 1, 'related': {}},
|
||||
{'model': Review, 'pk': 2, 'related': {}},
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.objects = defaultdict(lambda: [])
|
||||
for obj in self.OBJECTS:
|
||||
model = obj['model']
|
||||
self.objects[model.__name__].append(
|
||||
model.objects.get(pk=obj['pk']))
|
||||
|
||||
self.objects = dict(self.objects)
|
||||
|
||||
self.urls = tuple(o.get_url_path()
|
||||
for objs in self.objects.values()
|
||||
for o in objs)
|
||||
|
||||
self.fake_urls = (
|
||||
'https://addons.mozilla.org/en-US/firefox/addon/floorgl-plikzret/',
|
||||
'/foo/bar/zuq/',
|
||||
)
|
||||
|
||||
def assert_deleted(self):
|
||||
for obj in self.OBJECTS:
|
||||
assert not obj['model'].objects.filter(pk=obj['pk']).exists()
|
||||
|
||||
for model, pks in obj['related'].iteritems():
|
||||
assert not model.objects.filter(pk__in=pks).exists()
|
||||
|
||||
|
||||
class TestMassDeletion(MassDeletionTest):
|
||||
def test_relative_vs_absolute_urls(self):
|
||||
helper1 = MassDeleteHelper(urls=self.urls)
|
||||
helper2 = MassDeleteHelper(urls=map(absolutify, self.urls))
|
||||
|
||||
eq_(helper1.object_types_json,
|
||||
helper2.object_types_json)
|
||||
|
||||
def test_url_resolution(self):
|
||||
helper1 = MassDeleteHelper(urls=self.urls + self.fake_urls)
|
||||
helper2 = MassDeleteHelper(objects=self.objects)
|
||||
|
||||
eq_(helper1.unknown_urls, self.fake_urls)
|
||||
|
||||
eq_(helper1.object_types_json,
|
||||
helper2.object_types_json)
|
||||
|
||||
eq_(helper1.object_types,
|
||||
self.objects)
|
||||
|
||||
def test_related_objects(self):
|
||||
helper = MassDeleteHelper(objects=self.objects)
|
||||
for obj in self.OBJECTS:
|
||||
model = obj['model']
|
||||
related = dict((k, set(v))
|
||||
for k, v in obj['related'].iteritems())
|
||||
|
||||
for model, objs in helper.get_related_objects(model, [obj['pk']]):
|
||||
for o in objs:
|
||||
assert model in related
|
||||
assert o.pk in related[model]
|
||||
|
||||
related[model].remove(o.pk)
|
||||
|
||||
assert not any(related.values())
|
||||
|
||||
def test_deletion(self):
|
||||
helper = MassDeleteHelper(objects=self.objects)
|
||||
helper.delete_objects()
|
||||
|
||||
self.assert_deleted()
|
||||
|
|
@ -37,6 +37,8 @@ from zadmin.forms import DevMailerForm
|
|||
from zadmin.models import EmailPreviewTopic, ValidationJob, ValidationResult
|
||||
from zadmin.views import updated_versions, find_files
|
||||
|
||||
from .test_helpers import MassDeletionTest
|
||||
|
||||
|
||||
no_op_validation = dict(errors=0, warnings=0, notices=0, messages=[],
|
||||
compatibility_summary=dict(errors=0, warnings=0,
|
||||
|
@ -1151,6 +1153,59 @@ class TestEmailPreview(amo.tests.TestCase):
|
|||
'the subject', 'Hello Ivan Krsti\xc4\x87'])
|
||||
|
||||
|
||||
class TestMassDeletion(MassDeletionTest):
|
||||
def setUp(self):
|
||||
super(TestMassDeletion, self).setUp()
|
||||
|
||||
assert self.client.login(username='admin@mozilla.com',
|
||||
password='password')
|
||||
|
||||
def do_confirm(self):
|
||||
r = self.client.post(reverse('zadmin.mass_delete'),
|
||||
{'urls': '\n'.join(self.urls + self.fake_urls),
|
||||
'reason': 'Because I can.'})
|
||||
eq_(r.status_code, 200)
|
||||
return pq(r.content)
|
||||
|
||||
def test_confirm(self):
|
||||
doc = self.do_confirm()
|
||||
|
||||
eq_(doc('#unknown-urls li').text(),
|
||||
' '.join(self.fake_urls))
|
||||
print doc('ul[id^="delete-confirm"]').text()
|
||||
|
||||
def test_confirm_related_objects(self):
|
||||
doc = self.do_confirm()
|
||||
|
||||
for obj in self.OBJECTS:
|
||||
model = obj['model']
|
||||
inst = model.objects.get(pk=obj['pk'])
|
||||
|
||||
base_id = '#delete-confirm-%s-%d' % (model.__name__, obj['pk'])
|
||||
a = doc('%s > a' % base_id)
|
||||
|
||||
eq_(a.attr('href'), inst.get_url_path())
|
||||
eq_(a.text(), unicode(inst))
|
||||
|
||||
for model_, ids in obj['related'].iteritems():
|
||||
eq_(doc('%s-%s' % (base_id, model_.__name__)).text(),
|
||||
'%d %s objects' % (len(ids), model_.__name__))
|
||||
|
||||
# Very important.
|
||||
assert "I'm watching you." in doc('body').text()
|
||||
|
||||
def test_delete(self):
|
||||
doc = self.do_confirm()
|
||||
|
||||
r = self.client.post(reverse('zadmin.mass_delete.confirm'),
|
||||
{'objects': doc('#id_objects').val(),
|
||||
'reason': doc('#id_reason').val()})
|
||||
|
||||
self.assert3xx(r, reverse('zadmin.mass_delete.confirm'))
|
||||
|
||||
self.assert_deleted()
|
||||
|
||||
|
||||
class TestMonthlyPick(amo.tests.TestCase):
|
||||
fixtures = ['base/addon_3615', 'base/apps', 'base/users']
|
||||
|
||||
|
@ -1911,6 +1966,9 @@ class TestPerms(amo.tests.TestCase):
|
|||
eq_(self.client.get(reverse('zadmin.settings')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.flagged')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.langpacks')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete.confirm')).status_code,
|
||||
200)
|
||||
eq_(self.client.get(reverse('zadmin.addon-search')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.monthly_pick')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.features')).status_code, 200)
|
||||
|
@ -1930,6 +1988,9 @@ class TestPerms(amo.tests.TestCase):
|
|||
eq_(self.client.get(reverse('zadmin.settings')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.flagged')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.langpacks')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete.confirm')).status_code,
|
||||
200)
|
||||
eq_(self.client.get(reverse('zadmin.addon-search')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.monthly_pick')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.features')).status_code, 200)
|
||||
|
@ -1949,6 +2010,9 @@ class TestPerms(amo.tests.TestCase):
|
|||
eq_(self.client.get(reverse('zadmin.index')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.flagged')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.langpacks')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete.confirm')).status_code,
|
||||
403)
|
||||
eq_(self.client.get(reverse('zadmin.addon-search')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.settings')).status_code, 403)
|
||||
eq_(self.client.get(
|
||||
|
@ -1966,6 +2030,9 @@ class TestPerms(amo.tests.TestCase):
|
|||
eq_(self.client.get(reverse('zadmin.validation')).status_code, 200)
|
||||
eq_(self.client.get(reverse('zadmin.flagged')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.langpacks')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete.confirm')).status_code,
|
||||
403)
|
||||
eq_(self.client.get(reverse('zadmin.addon-search')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.settings')).status_code, 403)
|
||||
eq_(self.client.get(
|
||||
|
@ -1979,6 +2046,9 @@ class TestPerms(amo.tests.TestCase):
|
|||
eq_(self.client.get(reverse('zadmin.settings')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.flagged')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.langpacks')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.mass_delete.confirm')).status_code,
|
||||
403)
|
||||
eq_(self.client.get(reverse('zadmin.addon-search')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.monthly_pick')).status_code, 403)
|
||||
eq_(self.client.get(reverse('zadmin.features')).status_code, 403)
|
||||
|
|
|
@ -17,12 +17,15 @@ urlpatterns = patterns('',
|
|||
url('^addon/recalc-hash/(?P<file_id>\d+)/', views.recalc_hash,
|
||||
name='zadmin.recalc_hash'),
|
||||
url('^env$', views.env, name='amo.env'),
|
||||
url('^flagged', views.flagged, name='zadmin.flagged'),
|
||||
url('^langpacks', views.langpacks, name='zadmin.langpacks'),
|
||||
url('^flagged$', views.flagged, name='zadmin.flagged'),
|
||||
url('^langpacks$', views.langpacks, name='zadmin.langpacks'),
|
||||
url('^hera', views.hera, name='zadmin.hera'),
|
||||
url('^memcache$', views.memcache, name='zadmin.memcache'),
|
||||
url('^settings', views.show_settings, name='zadmin.settings'),
|
||||
url('^fix-disabled', views.fix_disabled_file, name='zadmin.fix-disabled'),
|
||||
url('^fix-disabled$', views.fix_disabled_file, name='zadmin.fix-disabled'),
|
||||
url('^mass-delete$', views.mass_delete, name='zadmin.mass_delete'),
|
||||
url('^mass-delete/confirm$', views.mass_delete_confirm,
|
||||
name='zadmin.mass_delete.confirm'),
|
||||
url(r'^validation/application_versions\.json$',
|
||||
views.application_versions_json,
|
||||
name='zadmin.application_versions_json'),
|
||||
|
|
|
@ -48,8 +48,10 @@ from . import tasks
|
|||
from .decorators import admin_required
|
||||
from .forms import (AddonStatusForm, BulkValidationForm, CompatForm,
|
||||
DevMailerForm, FeaturedCollectionFormSet, FileFormSet,
|
||||
JetpackUpgradeForm, MonthlyPickFormSet, NotifyForm,
|
||||
OAuthConsumerForm, YesImSure)
|
||||
JetpackUpgradeForm, MassDeleteForm, MassDeleteConfirmForm,
|
||||
MonthlyPickFormSet, NotifyForm, OAuthConsumerForm,
|
||||
YesImSure)
|
||||
from .helpers import MassDeleteHelper
|
||||
from .models import EmailPreviewTopic, ValidationJob, ValidationJobTally
|
||||
|
||||
log = commonware.log.getLogger('z.zadmin')
|
||||
|
@ -181,7 +183,7 @@ def env(request):
|
|||
return http.HttpResponse(u'<pre>%s</pre>' % (jinja2.escape(request)))
|
||||
|
||||
|
||||
@admin.site.admin_view
|
||||
@admin_required(reviewers=True)
|
||||
def fix_disabled_file(request):
|
||||
file_ = None
|
||||
if request.method == 'POST' and 'file' in request.POST:
|
||||
|
@ -194,6 +196,45 @@ def fix_disabled_file(request):
|
|||
{'file': file_, 'file_id': request.POST.get('file', '')})
|
||||
|
||||
|
||||
@admin_required
|
||||
def mass_delete(request):
|
||||
form = MassDeleteForm(request.POST or None)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
urls = form.cleaned_data['urls'].split()
|
||||
|
||||
helper = MassDeleteHelper(urls=urls)
|
||||
form = MassDeleteConfirmForm({'objects': helper.object_types_json,
|
||||
'reason': form.cleaned_data['reason']})
|
||||
|
||||
return render(request, 'zadmin/mass-delete-confirm.html',
|
||||
{'deletion_helper': helper,
|
||||
'unknown_urls': helper.unknown_urls,
|
||||
'confirm_form': form})
|
||||
|
||||
return render(request, 'zadmin/mass-delete.html',
|
||||
{'mass_delete_form': form})
|
||||
|
||||
|
||||
@admin_required
|
||||
def mass_delete_confirm(request):
|
||||
form = MassDeleteConfirmForm(request.POST)
|
||||
if request.method == 'POST':
|
||||
print form.is_valid()
|
||||
print form.cleaned_data
|
||||
if not form.is_valid():
|
||||
# Form is auto-generated. An invalid POST means user
|
||||
# meddling.
|
||||
raise PermissionDenied
|
||||
|
||||
helper = MassDeleteHelper(objects=form.cleaned_data['objects'],
|
||||
reason=form.cleaned_data['reason'])
|
||||
helper.delete_objects()
|
||||
|
||||
return redirect(reverse('zadmin.mass_delete.confirm'))
|
||||
|
||||
return render(request, 'zadmin/mass-delete-completed.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
@post_required
|
||||
@json_view
|
||||
|
|
|
@ -108,6 +108,22 @@ div.popup input[type="text"] {
|
|||
width: 588px;
|
||||
}
|
||||
|
||||
.proper-lists ul li {
|
||||
list-style: disc;
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.admin-mass-delete h1 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.admin-mass-delete h2,
|
||||
.admin-mass-delete h3 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.job-status {
|
||||
color: #777777;
|
||||
font-size: 11px;
|
||||
|
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 257 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 97 KiB |
Загрузка…
Ссылка в новой задаче