addons-server/apps/bandwagon/views.py

609 строки
21 KiB
Python

import functools
import hashlib
import os
from django import http
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect
import caching.base as caching
import commonware.log
import jingo
from tower import ugettext_lazy as _lazy, ugettext as _
import amo
from amo import messages
import sharing.views
from amo.decorators import (allow_mine, json_view, login_required,
post_required, restricted_content, write)
from amo.urlresolvers import reverse
from amo.utils import paginate, redirect_for_login, urlparams
from access import acl
from addons.models import Addon
from addons.views import BaseFilter
from api.utils import addon_to_dict
from tags.models import Tag
from translations.query import order_by_translation
from users.models import UserProfile
from .models import (Collection, CollectionAddon, CollectionWatcher,
CollectionVote, SPECIAL_SLUGS)
from . import forms, tasks
log = commonware.log.getLogger('z.collections')
def get_collection(request, username, slug):
if (slug in SPECIAL_SLUGS.values() and request.user.is_authenticated()
and request.amo_user.username == username):
return getattr(request.amo_user, slug + '_collection')()
else:
return get_object_or_404(Collection.objects,
author__username=username, slug=slug)
def owner_required(f=None, require_owner=True):
"""Requires collection to be owned, by someone."""
def decorator(func):
@functools.wraps(func)
def wrapper(request, username, slug, *args, **kw):
collection = get_collection(request, username, slug)
if acl.check_collection_ownership(request, collection,
require_owner=require_owner):
return func(request, collection, username, slug, *args, **kw)
else:
raise PermissionDenied
return wrapper
return decorator(f) if f else decorator
def legacy_redirect(request, uuid, edit=False):
# Nicknames have a limit of 30, so len == 36 implies a uuid.
key = 'uuid' if len(uuid) == 36 else 'nickname'
c = get_object_or_404(Collection.objects, **{key: uuid})
if edit:
return http.HttpResponseRedirect(c.edit_url())
to = c.get_url_path() + '?' + request.GET.urlencode()
return http.HttpResponseRedirect(to)
def legacy_directory_redirects(request, page):
sorts = {'editors_picks': 'featured', 'popular': 'popular',
'users': 'followers'}
loc = base = reverse('collections.list')
if page in sorts:
loc = urlparams(base, sort=sorts[page])
elif request.user.is_authenticated():
if page == 'mine':
loc = reverse('collections.user', args=[request.amo_user.username])
elif page == 'favorites':
loc = reverse('collections.following')
return http.HttpResponseRedirect(loc)
class CollectionFilter(BaseFilter):
opts = (('featured', _lazy(u'Featured')),
('followers', _lazy(u'Most Followers')),
('created', _lazy(u'Newest')))
extras = (('name', _lazy(u'Name')),
('updated', _lazy(u'Recently Updated')),
('popular', _lazy(u'Recently Popular')))
def filter(self, field):
qs = self.base_queryset
if field == 'featured':
return qs.filter(type=amo.COLLECTION_FEATURED)
elif field == 'followers':
return qs.order_by('-subscribers')
elif field == 'popular':
return qs.order_by('-weekly_subscribers')
elif field == 'updated':
return qs.order_by('-modified')
elif field == 'created':
return qs.order_by('-created')
elif field == 'name':
return order_by_translation(qs, 'name')
def get_filter(request, base=None):
if base is None:
base = Collection.objects.listed()
base = base.filter(Q(application=request.APP.id) | Q(application=None))
return CollectionFilter(request, base, key='sort', default='featured')
def render(request, template, data={}, extra={}):
data.update(dict(search_cat='collections'))
return jingo.render(request, template, data, **extra)
# TODO (potch): restore this when we do mobile bandwagon
# @mobile_template('bandwagon/{mobile/}collection_listing.html')
def collection_listing(request, base=None):
sort = request.GET.get('sort')
# We turn users into followers.
if sort == 'users':
return redirect(urlparams(reverse('collections.list'),
sort='followers'), permanent=True)
filter = get_filter(request, base)
# Counts are hard to cache automatically, and accuracy for this
# one is less important. Remember it for 5 minutes.
countkey = hashlib.md5(str(filter.qs.query) + '_count').hexdigest()
count = cache.get(countkey)
if count is None:
count = filter.qs.count()
cache.set(countkey, count, 300)
collections = paginate(request, filter.qs, count=count)
return render(request, 'bandwagon/impala/collection_listing.html',
dict(collections=collections, src='co-hc-sidebar',
dl_src='co-dp-sidebar', filter=filter, sort=sort,
sorting=filter.field))
def get_votes(request, collections):
if not request.user.is_authenticated():
return {}
q = CollectionVote.objects.filter(
user=request.amo_user, collection__in=[c.id for c in collections])
return dict((v.collection_id, v) for v in q)
@allow_mine
def user_listing(request, username):
author = get_object_or_404(UserProfile, username=username)
qs = (Collection.objects.filter(author__username=username)
.order_by('-created'))
mine = (request.user.is_authenticated() and
request.amo_user.username == username)
if mine:
page = 'mine'
else:
page = 'user'
qs = qs.filter(listed=True)
collections = paginate(request, qs)
votes = get_votes(request, collections.object_list)
return render(request, 'bandwagon/user_listing.html',
dict(collections=collections, collection_votes=votes,
page=page, author=author, filter=get_filter(request)))
class CollectionAddonFilter(BaseFilter):
opts = (('added', _lazy(u'Added')),
('popular', _lazy(u'Popularity')),
('name', _lazy(u'Name')))
def filter(self, field):
if field == 'added':
return self.base_queryset.order_by('collectionaddon__created')
elif field == 'name':
return order_by_translation(self.base_queryset, 'name')
elif field == 'popular':
return (self.base_queryset.order_by('-weekly_downloads')
.with_index(addons='downloads_type_idx'))
@allow_mine
def collection_detail(request, username, slug):
c = get_collection(request, username, slug)
if not c.listed:
if not request.user.is_authenticated():
return redirect_for_login(request)
if not acl.check_collection_ownership(request, c):
raise PermissionDenied
if request.GET.get('format') == 'rss':
return http.HttpResponsePermanentRedirect(c.feed_url())
base = Addon.objects.valid() & c.addons.all()
filter = CollectionAddonFilter(request, base,
key='sort', default='popular')
notes = get_notes(c)
# Go directly to CollectionAddon for the count to avoid joins.
count = CollectionAddon.objects.filter(
Addon.objects.valid_q(amo.VALID_STATUSES, prefix='addon__'),
collection=c.id)
addons = paginate(request, filter.qs, per_page=15, count=count.count())
# The add-on query is not related to the collection, so we need to manually
# hook them up for invalidation. Bonus: count invalidation.
keys = [addons.object_list.flush_key(), count.flush_key()]
caching.invalidator.add_to_flush_list({c.flush_key(): keys})
if c.author_id:
qs = Collection.objects.listed().filter(author=c.author)
others = amo.utils.randslice(qs, limit=4, exclude=c.id)
else:
others = []
# `perms` is defined in django.contrib.auth.context_processors. Gotcha!
user_perms = {
'view_stats': acl.check_ownership(request, c, require_owner=False),
}
tags = Tag.objects.filter(id__in=c.top_tags) if c.top_tags else []
return render(request, 'bandwagon/collection_detail.html',
{'collection': c, 'filter': filter, 'addons': addons,
'notes': notes, 'author_collections': others, 'tags': tags,
'user_perms': user_perms})
@json_view(has_trans=True)
@allow_mine
def collection_detail_json(request, username, slug):
c = get_collection(request, username, slug)
if not (c.listed or acl.check_collection_ownership(request, c)):
raise PermissionDenied
# We evaluate the QuerySet with `list` to work around bug 866454.
addons_dict = [addon_to_dict(a) for a in list(c.addons.valid())]
return {
'name': c.name,
'url': c.get_abs_url(),
'iconUrl': c.icon_url,
'addons': addons_dict
}
def get_notes(collection, raw=False):
# This might hurt in a big collection with lots of notes.
# It's a generator so we don't evaluate anything by default.
notes = CollectionAddon.objects.filter(collection=collection,
comments__isnull=False)
rv = {}
for note in notes:
# Watch out for comments in a language we didn't pick up.
if note.comments:
rv[note.addon_id] = (note.comments.localized_string if raw
else note.comments)
yield rv
@write
@login_required
def collection_vote(request, username, slug, direction):
c = get_collection(request, username, slug)
if request.method != 'POST':
return http.HttpResponseRedirect(c.get_url_path())
vote = {'up': 1, 'down': -1}[direction]
qs = (CollectionVote.objects.using('default')
.filter(collection=c, user=request.amo_user))
if qs:
cv = qs[0]
if vote == cv.vote: # Double vote => cancel.
cv.delete()
else:
cv.vote = vote
cv.save(force_update=True)
else:
CollectionVote.objects.create(collection=c, user=request.amo_user,
vote=vote)
if request.is_ajax():
return http.HttpResponse()
else:
return http.HttpResponseRedirect(c.get_url_path())
def initial_data_from_request(request):
return dict(author=request.amo_user, application_id=request.APP.id)
def collection_message(request, collection, option):
if option == 'add':
title = _('Collection created!')
msg = _("""Your new collection is shown below. You can <a
href="%(url)s">edit additional settings</a> if you'd
like.""") % {'url': collection.edit_url()}
elif option == 'update':
title = _('Collection updated!')
msg = _("""<a href="%(url)s">View your collection</a> to see the
changes.""") % {'url': collection.get_url_path()}
else:
raise ValueError('Incorrect option "%s", '
'takes only "add" or "update".' % option)
messages.success(request, title, msg, message_safe=True)
@write
@login_required
@restricted_content
def add(request):
"""Displays/processes a form to create a collection."""
data = {}
if request.method == 'POST':
form = forms.CollectionForm(request.POST, request.FILES,
initial=initial_data_from_request(request))
aform = forms.AddonsForm(request.POST)
if form.is_valid():
collection = form.save(default_locale=request.LANG)
collection.save()
if aform.is_valid():
aform.save(collection)
collection_message(request, collection, 'add')
log.info('Created collection %s' % collection.id)
return http.HttpResponseRedirect(collection.get_url_path())
else:
data['addons'] = Addon.objects.filter(pk__in=aform.clean_addon())
data['comments'] = aform.clean_addon_comment()
else:
form = forms.CollectionForm()
data.update(form=form, filter=get_filter(request))
return render(request, 'bandwagon/add.html', data)
@write
@login_required(redirect=False)
def ajax_new(request):
form = forms.CollectionForm(request.POST or None,
initial={'author': request.amo_user,
'application_id': request.APP.id},
)
if request.method == 'POST' and form.is_valid():
collection = form.save()
addon_id = request.REQUEST['addon_id']
collection.add_addon(Addon.objects.get(pk=addon_id))
log.info('Created collection %s' % collection.id)
return http.HttpResponseRedirect(reverse('collections.ajax_list')
+ '?addon_id=%s' % addon_id)
return jingo.render(request, 'bandwagon/ajax_new.html', {'form': form})
@login_required(redirect=False)
def ajax_list(request):
try:
addon_id = int(request.GET['addon_id'])
except (KeyError, ValueError):
return http.HttpResponseBadRequest()
# Get collections associated with this user
collections = Collection.objects.publishable_by(request.amo_user)
for collection in collections:
# See if the collections contains the addon
if addon_id in collection.addons.values_list('id', flat=True):
collection.has_addon = True
return jingo.render(request, 'bandwagon/ajax_list.html',
{'collections': collections})
@write
@login_required
@post_required
def collection_alter(request, username, slug, action):
c = get_collection(request, username, slug)
return change_addon(request, c, action)
def change_addon(request, collection, action):
if not acl.check_collection_ownership(request, collection):
raise PermissionDenied
try:
addon = get_object_or_404(Addon.objects, pk=request.POST['addon_id'])
except (ValueError, KeyError):
return http.HttpResponseBadRequest()
getattr(collection, action + '_addon')(addon)
log.info(u'%s: %s %s to collection %s' %
(request.amo_user, action, addon.id, collection.id))
if request.is_ajax():
url = '%s?addon_id=%s' % (reverse('collections.ajax_list'), addon.id)
else:
url = collection.get_url_path()
return http.HttpResponseRedirect(url)
@write
@login_required
@post_required
def ajax_collection_alter(request, action):
try:
c = get_object_or_404(Collection.objects, pk=request.POST['id'])
except (ValueError, KeyError):
return http.HttpResponseBadRequest()
return change_addon(request, c, action)
@write
@login_required
@owner_required(require_owner=False)
def edit(request, collection, username, slug):
is_admin = acl.action_allowed(request, 'Collections', 'Edit')
if request.method == 'POST':
initial = initial_data_from_request(request)
if collection.author_id: # Don't try to change the author.
initial['author'] = collection.author
form = forms.CollectionForm(request.POST, request.FILES,
initial=initial,
instance=collection)
if form.is_valid():
collection = form.save()
collection_message(request, collection, 'update')
log.info(u'%s edited collection %s' %
(request.amo_user, collection.id))
return http.HttpResponseRedirect(collection.edit_url())
else:
form = forms.CollectionForm(instance=collection)
qs = (CollectionAddon.objects.no_cache().using('default')
.filter(collection=collection))
meta = dict((c.addon_id, c) for c in qs)
addons = collection.addons.no_cache().all()
comments = get_notes(collection, raw=True).next()
if is_admin:
initial = dict(type=collection.type,
application=collection.application_id)
admin_form = forms.AdminForm(initial=initial)
else:
admin_form = None
data = dict(collection=collection,
form=form,
user=request.amo_user,
username=username,
slug=slug,
meta=meta,
filter=get_filter(request),
is_admin=is_admin,
admin_form=admin_form,
addons=addons,
comments=comments)
return render(request, 'bandwagon/edit.html', data)
@write
@login_required
@owner_required(require_owner=False)
@post_required
def edit_addons(request, collection, username, slug):
if request.method == 'POST':
form = forms.AddonsForm(request.POST)
if form.is_valid():
form.save(collection)
collection_message(request, collection, 'update')
log.info(u'%s added add-ons to %s' %
(request.amo_user, collection.id))
return http.HttpResponseRedirect(collection.edit_url() + '#addons-edit')
@write
@login_required
@owner_required
@post_required
def edit_contributors(request, collection, username, slug):
is_admin = acl.action_allowed(request, 'Collections', 'Edit')
if is_admin:
admin_form = forms.AdminForm(request.POST)
if admin_form.is_valid():
admin_form.save(collection)
form = forms.ContributorsForm(request.POST)
if form.is_valid():
form.save(collection)
collection_message(request, collection, 'update')
if form.cleaned_data['new_owner']:
return http.HttpResponseRedirect(collection.get_url_path())
return http.HttpResponseRedirect(collection.edit_url() + '#users-edit')
@write
@login_required
@owner_required
@post_required
def edit_privacy(request, collection, username, slug):
collection.listed = not collection.listed
collection.save()
log.info(u'%s changed privacy on collection %s' %
(request.amo_user, collection.id))
return http.HttpResponseRedirect(collection.get_url_path())
@write
@login_required
def delete(request, username, slug):
collection = get_object_or_404(Collection, author__username=username,
slug=slug)
if not acl.check_collection_ownership(request, collection, True):
log.info(u'%s is trying to delete collection %s'
% (request.amo_user, collection.id))
raise PermissionDenied
data = dict(collection=collection, username=username, slug=slug)
if request.method == 'POST':
if request.POST['sure'] == '1':
collection.delete()
log.info(u'%s deleted collection %s' %
(request.amo_user, collection.id))
url = reverse('collections.user', args=[username])
return http.HttpResponseRedirect(url)
else:
return http.HttpResponseRedirect(collection.get_url_path())
return render(request, 'bandwagon/delete.html', data)
@write
@login_required
@owner_required
@json_view
def delete_icon(request, collection, username, slug):
log.debug(u"User deleted collection (%s) icon " % slug)
tasks.delete_icon(os.path.join(collection.get_img_dir(),
'%d.png' % collection.id))
collection.icontype = ''
collection.save()
if request.is_ajax():
return {'icon': collection.icon_url}
else:
messages.success(request, _('Icon Deleted'))
return http.HttpResponseRedirect(collection.edit_url())
@login_required
@post_required
@json_view
def watch(request, username, slug):
"""
POST /collections/:user/:slug/watch to toggle the user's watching status.
For ajax, return {watching: true|false}. (reflects the new value)
Otherwise, redirect to the collection page.
"""
collection = get_collection(request, username, slug)
d = dict(user=request.amo_user, collection=collection)
qs = CollectionWatcher.objects.no_cache().using('default').filter(**d)
watching = not qs # Flip the bool since we're about to change it.
if qs:
qs.delete()
else:
CollectionWatcher.objects.create(**d)
if request.is_ajax():
return {'watching': watching}
else:
return http.HttpResponseRedirect(collection.get_url_path())
def share(request, username, slug):
collection = get_collection(request, username, slug)
return sharing.views.share(request, collection,
name=collection.name,
description=collection.description)
@login_required
def following(request):
qs = (Collection.objects.filter(following__user=request.amo_user)
.order_by('-following__created'))
collections = paginate(request, qs)
votes = get_votes(request, collections.object_list)
return render(request, 'bandwagon/user_listing.html',
dict(collections=collections, votes=votes,
page='following', filter=get_filter(request)))
@login_required
@allow_mine
def mine(request, username=None, slug=None):
if slug is None:
return user_listing(request, username)
else:
return collection_detail(request, username, slug)