evorepo/todolists/views.py
2023-11-03 12:35:59 +01:00

257 lines
8.7 KiB
Python

import json
from operator import attrgetter
from django import forms
from django.core.mail import send_mail
from django.db import transaction
from django.http import HttpResponse
from django.shortcuts import get_list_or_404, get_object_or_404, redirect, render
from django.template import loader
from django.utils.timezone import now
from django.views.decorators.cache import never_cache
from django.views.generic import DeleteView, ListView
from main.models import Package, Repo
from main.utils import find_unique_slug
from packages.utils import PackageJSONEncoder, attach_maintainers
from .models import Todolist, TodolistPackage
from .utils import attach_staging, get_annotated_todolists
class TodoListForm(forms.ModelForm):
raw = forms.CharField(label='Packages', required=False,
help_text='(one per line)',
widget=forms.Textarea(attrs={'rows': '20', 'cols': '60'}))
def package_names(self):
return {s.strip() for s in self.cleaned_data['raw'].split("\n")}
def packages(self):
return Package.objects.normal().filter(
pkgname__in=self.package_names(),
repo__testing=False, repo__staging=False).order_by('arch')
class Meta:
model = Todolist
fields = ('name', 'kind', 'description', 'raw')
@never_cache
def flag(request, slug, pkg_id):
todolist = get_object_or_404(Todolist, slug=slug)
tlpkg = get_object_or_404(TodolistPackage, id=pkg_id, removed__isnull=True)
# TODO: none of this; require absolute value on submit
if tlpkg.status == TodolistPackage.INCOMPLETE:
tlpkg.status = TodolistPackage.COMPLETE
else:
tlpkg.status = TodolistPackage.INCOMPLETE
tlpkg.user = request.user
tlpkg.save(update_fields=('status', 'user', 'last_modified'))
if request.headers.get('x-requested-with') == 'XMLHttpRequest':
data = {
'status': tlpkg.get_status_display(),
'css_class': tlpkg.status_css_class(),
}
return HttpResponse(json.dumps(data), content_type='application/json')
return redirect(todolist)
def view(request, slug):
todolist = get_object_or_404(Todolist, slug=slug)
svn_roots = Repo.objects.values_list(
'svn_root', flat=True).order_by().distinct()
# we don't hold onto the result, but the objects are the same here,
# so accessing maintainers in the template is now cheap
attach_maintainers(todolist.packages())
attach_staging(todolist.packages(), todolist.pk)
arches = {tp.arch for tp in todolist.packages()}
repos = {tp.repo for tp in todolist.packages()}
context = {
'list': todolist,
'svn_roots': svn_roots,
'arches': sorted(arches),
'repos': sorted(repos),
}
return render(request, 'todolists/view.html', context)
def list_pkgbases(request, slug, svn_root):
'''Used to make bulk moves of packages a lot easier.'''
todolist = get_object_or_404(Todolist, slug=slug)
repos = get_list_or_404(Repo, svn_root=svn_root)
pkgbases = TodolistPackage.objects.values_list(
'pkgbase', flat=True).filter(
todolist=todolist, repo__in=repos, removed__isnull=True).order_by(
'pkgbase').distinct()
return HttpResponse('\n'.join(pkgbases), content_type='text/plain')
class TodolistListView(ListView):
context_object_name = "lists"
template_name = "todolists/list.html"
paginate_by = 50
def get_queryset(self):
return get_annotated_todolists()
@never_cache
def add(request):
if request.POST:
form = TodoListForm(request.POST)
if form.is_valid():
new_packages = create_todolist_packages(form, creator=request.user)
send_todolist_emails(form.instance, new_packages)
return redirect(form.instance)
else:
form = TodoListForm()
page_dict = {
'title': 'Add Todo List',
'description': '',
'form': form,
'submit_text': 'Create List'
}
return render(request, 'general_form.html', page_dict)
# TODO: this calls for transaction management and async emailing
@never_cache
def edit(request, slug):
todo_list = get_object_or_404(Todolist, slug=slug)
if request.POST:
form = TodoListForm(request.POST, instance=todo_list)
if form.is_valid():
new_packages = create_todolist_packages(form)
send_todolist_emails(todo_list, new_packages)
return redirect(todo_list)
else:
form = TodoListForm(instance=todo_list,
initial={'packages': todo_list.raw})
page_dict = {
'title': f'Edit Todo List: {todo_list.name}',
'description': '',
'form': form,
'submit_text': 'Save List'
}
return render(request, 'general_form.html', page_dict)
class DeleteTodolist(DeleteView):
model = Todolist
# model in main == assumes name 'main/todolist_confirm_delete.html'
template_name = 'todolists/todolist_confirm_delete.html'
success_url = '/todo/'
@transaction.atomic
def create_todolist_packages(form, creator=None):
package_names = form.package_names()
packages = form.packages()
timestamp = now()
if creator:
# todo list is new, populate creator and slug fields
todolist = form.save(commit=False)
todolist.creator = creator
todolist.slug = find_unique_slug(Todolist, todolist.name)
todolist.save()
else:
# todo list already existed
form.save()
todolist = form.instance
# first mark removed any packages not in the new list
to_remove = set()
for todo_pkg in todolist.packages():
if todo_pkg.pkg and todo_pkg.pkg not in packages:
to_remove.add(todo_pkg.pk)
elif todo_pkg.pkgname not in package_names:
to_remove.add(todo_pkg.pk)
TodolistPackage.objects.filter(
pk__in=to_remove).update(removed=timestamp)
# Add (or mark unremoved) any packages in the new packages list
todo_pkgs = []
for package in packages:
# ensure get_or_create uses the fields in our unique constraint
defaults = {
'pkg': package,
'pkgbase': package.pkgbase,
'repo': package.repo,
}
todo_pkg, created = TodolistPackage.objects.get_or_create(
todolist=todolist,
pkgname=package.pkgname,
arch=package.arch,
defaults=defaults)
if created:
todo_pkgs.append(todo_pkg)
else:
save = False
if todo_pkg.removed is not None:
todo_pkg.removed = None
save = True
if todo_pkg.pkg != package:
todo_pkg.pkg = package
save = True
if save:
todo_pkg.save()
return todo_pkgs
def send_todolist_emails(todo_list, new_packages):
'''Sends emails to package maintainers notifying them that packages have
been added to a todo list.'''
# start by flipping the incoming list on its head: we want a list of
# involved maintainers and the packages they need to be notified about.
orphan_packages = []
maint_packages = {}
for todo_package in new_packages:
maints = todo_package.pkg.maintainers.values_list('email', flat=True)
if not maints:
orphan_packages.append(todo_package)
else:
for maint in maints:
maint_packages.setdefault(maint, []).append(todo_package)
for maint, packages in maint_packages.items():
packages = sorted(packages, key=attrgetter('pkgname', 'arch'))
ctx = {
'todo_packages': packages,
'todolist': todo_list,
}
template = loader.get_template('todolists/email_notification.txt')
send_mail('Packages added to todo list \'%s\'' % todo_list.name,
template.render(ctx),
'Arch Website Notification <nobody@archlinux.org>',
[maint],
fail_silently=True)
class TodoListJSONEncoder(PackageJSONEncoder):
def default(self, obj):
if isinstance(obj, Todolist):
return {
'id': obj.pk,
'name': obj.name,
'description': obj.description,
'created': obj.created,
'last_modified': obj.last_modified,
'packages': [tpkg for tpkg in obj.packages() if tpkg.pkg],
'kind': obj.kind_str
}
return super(TodoListJSONEncoder, self).default(obj)
def view_json(request, slug):
todolist = get_object_or_404(Todolist, slug=slug)
to_json = json.dumps(todolist, ensure_ascii=False, cls=TodoListJSONEncoder)
return HttpResponse(to_json, content_type='application/json')
# vim: set ts=4 sw=4 et: