openach/views/evidence.py
import logging
from collections import defaultdict
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods, require_safe
from openach.auth import check_edit_authorization
from openach.decorators import account_required, cache_if_anon
from openach.forms import EvidenceForm, EvidenceSourceForm
from openach.models import (
AnalystSourceTag,
Board,
BoardFollower,
Evidence,
EvidenceSource,
EvidenceSourceTag,
)
from openach.tasks import fetch_source_metadata
from .notifications import notify_add, notify_edit
from .util import remove_and_redirect
PAGE_CACHE_TIMEOUT_SECONDS = getattr(settings, "PAGE_CACHE_TIMEOUT_SECONDS", 60)
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
@require_safe
@account_required
@cache_if_anon(PAGE_CACHE_TIMEOUT_SECONDS)
def evidence_detail(request, evidence_id):
"""Return a view displaying detailed information about a piece of evidence and its sources."""
# NOTE: cannot cache page for logged in users b/c comments section contains CSRF and other protection mechanisms.
evidence = get_object_or_404(Evidence, pk=evidence_id)
available_tags = EvidenceSourceTag.objects.all()
sources = (
EvidenceSource.objects.filter(evidence=evidence)
.order_by("-source_date")
.select_related("uploader")
)
all_tags = AnalystSourceTag.objects.filter(source__in=sources)
source_tags = defaultdict(list)
user_tags = defaultdict(list)
for tag in all_tags:
key = (tag.source_id, tag.tag_id)
source_tags[key].append(tag)
if tag.tagger_id == request.user.id:
user_tags[key].append(tag)
return render(
request,
"boards/evidence_detail.html",
{
"evidence": evidence,
"sources": sources,
"source_tags": source_tags,
"user_tags": user_tags,
"available_tags": available_tags,
"meta_description": _("Analysis of evidence: {description}").format(
description=evidence.evidence_desc
),
},
)
@require_http_methods(["HEAD", "GET", "POST"])
@login_required
def add_evidence(request, board_id):
"""Return a view of adding evidence (with a source), or handle the form submission."""
board = get_object_or_404(Board, pk=board_id)
if "add_elements" not in board.permissions.for_user(request.user):
raise PermissionDenied()
require_source = getattr(settings, "`EVIDENCE_REQUIRE_SOURCE`", True)
if request.method == "POST":
evidence_form = EvidenceForm(request.POST)
source_form = EvidenceSourceForm(request.POST, require=require_source)
if evidence_form.is_valid() and source_form.is_valid():
with transaction.atomic():
evidence = evidence_form.save(commit=False)
evidence.board = board
evidence.creator = request.user
evidence.save()
if source_form.cleaned_data.get("source_url"):
source = source_form.save(commit=False)
source.evidence = evidence
source.uploader = request.user
source.save()
fetch_source_metadata.delay(source.id)
BoardFollower.objects.update_or_create(
board=board,
user=request.user,
defaults={
"is_contributor": True,
},
)
notify_add(board, actor=request.user, action_object=evidence)
return HttpResponseRedirect(reverse("openach:detail", args=(board.id,)))
else:
evidence_form = EvidenceForm()
source_form = EvidenceSourceForm(
require=require_source, initial={"corroborating": True}
)
return render(
request,
"boards/add_evidence.html",
{
"board": board,
"evidence_form": evidence_form,
"source_form": source_form,
},
)
@require_http_methods(["HEAD", "GET", "POST"])
@login_required
def edit_evidence(request, evidence_id):
"""Return a view for editing a piece of evidence, or handle for submission."""
evidence = get_object_or_404(Evidence, pk=evidence_id)
# don't care that the board might have been removed
board = evidence.board
check_edit_authorization(request, board=board, has_creator=evidence)
if request.method == "POST":
form = EvidenceForm(request.POST, instance=evidence)
if "remove" in form.data:
return remove_and_redirect(request, evidence, evidence.evidence_desc)
elif form.is_valid():
form.save()
messages.success(request, _("Updated evidence description and date."))
notify_edit(board, actor=request.user, action_object=evidence)
return HttpResponseRedirect(
reverse("openach:evidence_detail", args=(evidence.id,))
)
else:
form = EvidenceForm(instance=evidence)
return render(
request,
"boards/edit_evidence.html",
{
"form": form,
"evidence": evidence,
"board": board,
"allow_remove": getattr(settings, "EDIT_REMOVE_ENABLED", True),
},
)
@require_http_methods(["HEAD", "GET", "POST"])
@login_required
def add_source(request, evidence_id):
"""Return a view for adding a corroborating/contradicting source, or handle form submission."""
evidence = get_object_or_404(Evidence, pk=evidence_id)
if request.method == "POST":
form = EvidenceSourceForm(request.POST)
if form.is_valid():
source = form.save(commit=False)
source.evidence = evidence
source.uploader = request.user
source.save()
fetch_source_metadata.delay(source.id)
return HttpResponseRedirect(
reverse("openach:evidence_detail", args=(evidence_id,))
)
else:
corroborating = form.data["corroborating"] == "True"
else:
corroborating = (
request.GET.get("kind") is None or request.GET.get("kind") != "conflicting"
)
form = EvidenceSourceForm(initial={"corroborating": corroborating})
return render(
request,
"boards/add_source.html",
{"form": form, "evidence": evidence, "corroborating": corroborating},
)
@require_http_methods(["HEAD", "GET", "POST"])
@login_required
def toggle_source_tag(request, evidence_id, source_id):
"""Toggle source tag for the given source and redirect to the evidence detail page for the associated evidence."""
# May want to put in a sanity check here that source_id actually corresponds to evidence_id
# Inefficient to have to do the DB lookup before making a modification. May want to have the client pass in
# whether or not they're adding/removing the tag
if request.method == "POST":
with transaction.atomic():
source = get_object_or_404(EvidenceSource, pk=source_id)
tag = EvidenceSourceTag.objects.get(tag_name=request.POST["tag"])
user_tag = AnalystSourceTag.objects.filter(
source=source, tagger=request.user, tag=tag
)
if user_tag.count() > 0:
user_tag.delete()
messages.success(
request,
_('Removed "{name}" tag from source.').format(name=tag.tag_name),
)
else:
AnalystSourceTag.objects.create(
source=source, tagger=request.user, tag=tag
)
messages.success(
request,
_('Added "{name}" tag to source.').format(name=tag.tag_name),
)
return HttpResponseRedirect(
reverse("openach:evidence_detail", args=(evidence_id,))
)
else:
# Redirect to the form where the user can toggle a source tag
return HttpResponseRedirect(
reverse("openach:evidence_detail", args=(evidence_id,))
)