views.py 10.3 KB
Newer Older
1
#   views.py — packages views
Jonny Lamb's avatar
Jonny Lamb committed
2
#
3
4
#   This file is part of debexpo -
#   https://salsa.debian.org/mentors.debian.net-team/debexpo
Jonny Lamb's avatar
Jonny Lamb committed
5
#
Jonny Lamb's avatar
Jonny Lamb committed
6
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
7
#   Copyright © 2010 Jan Dittberner <jandd@debian.org>
Arno Töll's avatar
Arno Töll committed
8
#               2011 Arno Töll <debian@toell.net>
9
#               2019 Baptiste Beauplat <lyknode@cilg.org>
Jonny Lamb's avatar
Jonny Lamb committed
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

import logging
Arno Töll's avatar
Arno Töll committed
33
34
import datetime

35
from django.conf import settings
36
37
38
from django.http import HttpResponseForbidden, HttpResponseRedirect, \
    HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404
39
40
41
42
from django.urls import reverse
from django.utils.translation import gettext as _, get_language
from django.contrib.syndication.views import Feed
from django.contrib.auth.decorators import login_required
43
44
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework_extensions.mixins import NestedViewSetMixin
Jonny Lamb's avatar
Jonny Lamb committed
45

46
from debexpo.packages.models import PackageUpload, Package, SourcePackage
47
48
from debexpo.packages.serializers import PackageSerializer, \
    PackageUploadSerializer
49
from debexpo.comments.forms import CommentForm
50
from debexpo.repository.tasks import remove_from_repository
51
52
from debexpo.tools.gitstorage import GitStorage
from debexpo.bugs.models import Bug
53
from debexpo.packages.tasks import remove_uploads
Jonny Lamb's avatar
Jonny Lamb committed
54
55
56

log = logging.getLogger(__name__)

57
58
59
60
61
62
63
LIST_MODELS = [
    'distribution',
    'component',
    'section',
    'priority',
]

Jonny Lamb's avatar
Jonny Lamb committed
64

Arno Töll's avatar
Arno Töll committed
65
66
67
68
69
70
class PackageGroups(object):
    """
    A helper class to hold packages matching a certain time criteria
    """
    def __init__(self, label, deltamin, deltamax, packages):
        self.label = label
71
72
        self.packages = []

73
        if (deltamin is not None and deltamax is not None):
74
75
76
77
78
            self.packages = [
                x for x in packages if
                x.packageupload_set.latest('uploaded').uploaded <= deltamin and
                x.packageupload_set.latest('uploaded').uploaded > deltamax
            ]
79
        elif (deltamin is None and deltamax is not None):
80
81
82
83
            self.packages = [
                x for x in packages if
                x.packageupload_set.latest('uploaded').uploaded > deltamax
            ]
84
85
        # Last actual possibility
        elif (deltamin is not None and deltamax is None):  # pragma: no branch
86
87
88
89
            self.packages = [
                x for x in packages if
                x.packageupload_set.latest('uploaded').uploaded <= deltamin
            ]
90
91


92
93
94
def _get_packages(key=None, value=None):
    """
    Returns a list of packages that fit the filters.
95

96
97
    ``key``
        Any key of a Package, PackageUpload or SourcePackage
98

99
100
    ``value``
        Corresponding value to filter by
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    """
    query = Package.objects
    name = key

    # Use name lookup for 'list' models
    if key in LIST_MODELS:
        key = '{}__name'.format(key)

    # Use email lookup for uploader
    if key == 'uploader':
        key = 'uploader__email'

    if name in [f.name for f in Package._meta.get_fields()]:
        query = query.filter(**{key: value})
    elif name in [f.name for f in PackageUpload._meta.get_fields()]:
        query = query.filter(**{'packageupload__' + key: value})
    elif name in [f.name for f in SourcePackage._meta.get_fields()]:
        query = query.filter(**{'packageupload__sourcepackage__' + key: value})
    elif name is not None:
120
121
        query = query.none()
        log.warning('Could not apply filter: %s', key)
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150

    return set(query.all())


def _get_timedeltas(packages):
    deltas = []
    now = datetime.datetime.now(datetime.timezone.utc)
    deltas.append(PackageGroups(_("Today"), None, now -
                                datetime.timedelta(days=1),
                                packages))
    deltas.append(PackageGroups(_("Yesterday"), now -
                                datetime.timedelta(days=1),
                                now - datetime.timedelta(days=2),
                                packages))
    deltas.append(PackageGroups(_("Some days ago"), now -
                                datetime.timedelta(days=2),
                                now - datetime.timedelta(days=7),
                                packages))
    deltas.append(PackageGroups(_("Older packages"), now -
                                datetime.timedelta(days=7),
                                now - datetime.timedelta(days=30),
                                packages))
    deltas.append(PackageGroups(_("Uploaded long ago"), now -
                                datetime.timedelta(days=30),
                                None, packages))
    return deltas


def package(request, name):
151
    package = get_object_or_404(Package, name=name)
152
    form = CommentForm()
153
154
155
156

    return render(request, 'package.html', {
        'settings': settings,
        'package': package,
157
        'comment_form': form,
158
159
160
    })


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@login_required
def delete_upload(request, name, upload):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])

    upload = get_object_or_404(PackageUpload, id=upload)
    package = upload.package.name

    if (request.user != upload.uploader and not
            request.user.is_superuser):
        return HttpResponseForbidden()

    remove_uploads([upload])

    try:
        Package.objects.get(name=package)
    except Package.DoesNotExist:
        return HttpResponseRedirect(reverse('packages_my'))
    else:
        return HttpResponseRedirect(reverse('package', args=[package]))


183
184
185
186
187
188
189
190
191
192
193
194
195
196
@login_required
def delete_package(request, name):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])

    package = get_object_or_404(Package, name=name)

    if (request.user not in package.get_uploaders() and not
            request.user.is_superuser):
        return HttpResponseForbidden()

    package.delete()
    log.info('Package deleted: {}'.format(name))

197
198
199
200
201
202
    git_storage_path = getattr(settings, 'GIT_STORAGE', None)

    if git_storage_path:
        git_storage = GitStorage(git_storage_path, name)
        git_storage.remove()

203
    remove_from_repository.delay(name)
204
    Bug.objects.remove_bugs(package)
205

206
207
208
209
210
211
212
213
214
215
216
217
218
219
    return HttpResponseRedirect(reverse('packages_my'))


@login_required
def sponsor_package(request, name):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])

    package = get_object_or_404(Package, name=name)

    if request.user not in package.get_uploaders():
        return HttpResponseForbidden()

    package.needs_sponsor = not package.needs_sponsor
220
    package.full_clean()
221
222
223
224
225
    package.save()
    log.info('Toogle needs sponsor for: {} ({})'.format(name,
                                                        package.needs_sponsor))

    return HttpResponseRedirect(reverse('package', args=[name]))
226
227
228


def packages(request, key=None, value=None):
229
    # List of packages to show in the page.
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
    packages = _get_packages(key, value)
    feed = request.build_absolute_uri() + 'feed/'

    if key:
        title = _('Packages for {} {}').format(key, value)
    else:
        title = _('Package list')

    # Render the page.
    return render(request, 'packages.html', {
        'settings': settings,
        'packages': packages,
        'deltas': _get_timedeltas(packages),
        'package_title': title,
        'feed_url': feed
    })


class PackagesFeed(Feed):
    title = _('%s packages' % settings.SITE_NAME)
    description = _('A feed of packages on %s' % settings.SITE_NAME)
    language = get_language()

    def get_object(self, request, key=None, value=None, feed=None):
        self.request = request
        return _get_packages(key, value)

    def link(self, packages):
258
        return self.request.build_absolute_uri(reverse('packages'))
259
260
261
262
263
264
265
266
267

    def items(self, packages):
        return packages

    def item_title(self, item):
        return '%s %s' % (
            item.name, item.packageupload_set.latest('uploaded').version)

    def item_link(self, item):
268
269
        return self.request.build_absolute_uri(
                reverse('package', kwargs={'name': item.name}))
270
271
272
273
274
275
276
277
278
279
280

    def item_description(self, item):
        desc = _('Package {} uploaded by {}.').format(
            item.name,
            item.packageupload_set.latest('uploaded').uploader.name)
        desc += '<br/><br/>'

        if item.needs_sponsor:
            desc += _('Uploader is currently looking for a sponsor.')
        else:
            desc += _('Uploader is currently not looking for a sponsor.')
281

282
283
        binary_package = item.packageupload_set.latest('uploaded') \
            .binarypackage_set.filter(name=item.name)
284

285
286
287
        if binary_package:
            desc += '<br/><br/>' + binary_package.get().description \
                    .replace('\n', '<br/>')
288

289
        return desc
290

291

292
293
294
@login_required
def packages_my(request):
    return packages(request, 'uploader', request.user.email)
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309


class PackageViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
    queryset = Package.objects.all()
    serializer_class = PackageSerializer
    filterset_fields = set(serializer_class.Meta.fields) \
        .difference(('uploaders', 'versions', 'id',))


class PackageUploadViewSet(NestedViewSetMixin, ReadOnlyModelViewSet):
    queryset = PackageUpload.objects.all()
    serializer_class = PackageUploadSerializer
    filterset_fields = set(serializer_class.Meta.fields) \
        .difference(('id', 'distribution', 'component', 'closes', 'uploader',
                     'package'))