Exemple #1
0
def test_csv_export(now, client, answer_type, answer):
    im = ImageFactory()

    rs = ReaderStudyFactory()
    rs.images.add(im)
    rs.save()

    editor = UserFactory()
    rs.add_editor(editor)

    reader = UserFactory()
    rs.add_reader(reader)

    q = QuestionFactory(
        question_text="foo", reader_study=rs, answer_type=answer_type
    )

    a = AnswerFactory(question=q, answer=answer)
    a.images.add(im)
    a.save()

    response = get_view_for_user(
        viewname="api:reader-study-export-answers",
        reverse_kwargs={"pk": rs.pk},
        user=editor,
        client=client,
        method=client.get,
        content_type="application/json",
    )

    headers = str(response.serialize_headers())
    content = str(response.content)

    assert response.status_code == 200
    assert "Content-Type: text/csv" in headers
    assert (
        f'filename="{rs.slug}-answers-2020-01-01T00:00:00+00:00.csv"'
        in headers
    )
    assert a.question.question_text in content
    assert a.question.get_answer_type_display() in content
    assert str(a.question.required) in content
    assert a.question.get_image_port_display() in content
    if isinstance(answer, dict):
        for key in answer:
            assert key in content
    else:
        assert re.sub(r"[\n\r\t]", " ", str(a.answer)) in content
    assert im.name in content
    assert a.creator.username in content

    response = get_view_for_user(
        viewname="api:reader-study-export-answers",
        reverse_kwargs={"pk": rs.pk},
        user=reader,
        client=client,
        method=client.get,
        content_type="application/json",
    )
    assert response.status_code == 404
def test_ground_truth_is_excluded(client):
    im = ImageFactory()
    rs = ReaderStudyFactory()
    rs.images.add(im)

    editor = UserFactory()
    rs.add_editor(editor)
    rs.add_reader(editor)

    q = QuestionFactory(reader_study=rs, answer_type=Question.AnswerType.BOOL)

    a1 = AnswerFactory(question=q,
                       creator=editor,
                       answer=True,
                       is_ground_truth=True)
    a1.images.add(im)

    a2 = AnswerFactory(question=q,
                       creator=editor,
                       answer=True,
                       is_ground_truth=False)
    a2.images.add(im)

    response = get_view_for_user(
        viewname="api:reader-studies-answer-mine",
        user=editor,
        client=client,
        method=client.get,
        content_type="application/json",
    )
    results = response.json()["results"]
    assert len(results) == 1
    assert results[0]["pk"] == str(a2.pk)
Exemple #3
0
def test_user_upload_to_display_set_without_interface(client, settings):
    # Override the celery settings
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    user = UserFactory()
    rs = ReaderStudyFactory(use_display_sets=False)
    rs.add_editor(user=user)
    ci = ComponentInterface.objects.filter(slug="generic-overlay").get()
    civ = ComponentInterfaceValueFactory(interface=ci)
    ds = DisplaySetFactory(reader_study=rs)
    ds.values.add(civ)
    assert ds.values.count() == 1

    upload = create_upload_from_file(
        file_path=Path(__file__).parent / "resources" / "image10x10x10.mha",
        creator=user,
    )
    with capture_on_commit_callbacks(execute=True):
        response = get_view_for_user(
            viewname="api:upload-session-list",
            user=user,
            client=client,
            method=client.post,
            content_type="application/json",
            data={
                "uploads": [upload.api_url],
                "display_set": ds.pk
            },
            HTTP_X_FORWARDED_PROTO="https",
        )

    assert response.status_code == 400
    assert ("An interface needs to be defined to upload to a display set."
            in response.json()["non_field_errors"])
Exemple #4
0
def test_assign_score(settings):
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    rs = ReaderStudyFactory(use_display_sets=False)
    im = ImageFactory()
    q1 = QuestionFactory(reader_study=rs)
    q2 = QuestionFactory(reader_study=rs,
                         answer_type=Question.AnswerType.MULTIPLE_CHOICE)
    e, r1, r2 = UserFactory(), UserFactory(), UserFactory()

    rs.images.add(im)
    rs.add_editor(e)
    rs.add_reader(r1)
    rs.add_reader(r2)

    with capture_on_commit_callbacks(execute=True):
        a1 = AnswerFactory(question=q1, creator=r1, answer="foo")
    a1.images.add(im)
    assert a1.score is None

    with capture_on_commit_callbacks(execute=True):
        gt = AnswerFactory(question=q1,
                           creator=e,
                           answer="foo",
                           is_ground_truth=True)
        gt.images.add(im)
    a1.refresh_from_db()
    assert a1.score == 1.0

    with capture_on_commit_callbacks(execute=True):
        a2 = AnswerFactory(question=q1, creator=r2, answer="foo")
        a2.images.add(im)
    a2.refresh_from_db()
    assert a2.score == 1.0

    with capture_on_commit_callbacks(execute=True):
        a1 = AnswerFactory(question=q2, creator=r1, answer=[])
        a1.images.add(im)
    a1.refresh_from_db()
    assert a1.score is None

    with capture_on_commit_callbacks(execute=True):
        gt = AnswerFactory(question=q2,
                           creator=e,
                           answer=[],
                           is_ground_truth=True)
        gt.images.add(im)
    a1.refresh_from_db()
    assert a1.score == 1.0

    with capture_on_commit_callbacks(execute=True):
        a2 = AnswerFactory(question=q2, creator=r2, answer=[])
        a2.images.add(im)
    a2.refresh_from_db()
    assert a2.score == 1.0
Exemple #5
0
def test_question_delete_disabled_for_questions_with_answers(client):
    rs = ReaderStudyFactory(use_display_sets=False)
    r1, editor = UserFactory(), UserFactory()
    rs.add_reader(r1)
    rs.add_editor(editor)
    q = QuestionFactory(
        reader_study=rs,
        question_text="q1",
        answer_type=Question.AnswerType.BOOL,
    )
    AnswerFactory(creator=r1, question=q, answer=True)

    assert Answer.objects.count() == 1
    assert Question.objects.count() == 1
    assert not q.is_fully_editable

    response = get_view_for_user(
        viewname="reader-studies:question-delete",
        client=client,
        method=client.post,
        reverse_kwargs={
            "slug": rs.slug,
            "pk": q.pk
        },
        user=editor,
    )

    assert response.status_code == 403
    assert Question.objects.count() == 1

    # if answer is deleted, deletion of the question is possible again
    get_view_for_user(
        viewname="reader-studies:answers-remove",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"user": r1.id},
        follow=True,
        user=editor,
    )

    assert Answer.objects.count() == 0

    response = get_view_for_user(
        viewname="reader-studies:question-delete",
        client=client,
        method=client.post,
        reverse_kwargs={
            "slug": rs.slug,
            "pk": q.pk
        },
        user=editor,
    )
    assert response.status_code == 302
    assert Question.objects.count() == 0
Exemple #6
0
def test_remove_image_api_view(client):
    rs = ReaderStudyFactory()
    reader, editor = UserFactory(), UserFactory()
    rs.add_reader(reader)
    rs.add_editor(editor)

    response = get_view_for_user(
        viewname="api:reader-study-remove-image",
        reverse_kwargs={"pk": rs.pk},
        user=reader,
        client=client,
        method=client.patch,
        data={"image": 1},
        content_type="application/json",
        follow=True,
    )

    assert response.status_code == 403

    response = get_view_for_user(
        viewname="api:reader-study-remove-image",
        reverse_kwargs={"pk": rs.pk},
        user=editor,
        client=client,
        method=client.patch,
        data={"image": 1},
        content_type="application/json",
        follow=True,
    )

    assert response.status_code == 200
    assert "Image could not be removed from reader study." in str(
        response.content
    )

    im = ImageFactory()
    rs.images.add(im)

    assert im in rs.images.all()

    response = get_view_for_user(
        viewname="api:reader-study-remove-image",
        reverse_kwargs={"pk": rs.pk},
        user=editor,
        client=client,
        method=client.patch,
        data={"image": im.pk},
        content_type="application/json",
        follow=True,
    )

    assert response.status_code == 200
    assert "Image removed from reader study." in str(response.content)
    assert im not in rs.images.all()
Exemple #7
0
def test_session_with_user_upload_to_readerstudy(client, settings):
    # Override the celery settings
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    user = UserFactory()
    rs = ReaderStudyFactory(use_display_sets=False)
    rs.add_editor(user=user)

    upload = create_upload_from_file(
        file_path=Path(__file__).parent / "resources" / "image10x10x10.mha",
        creator=user,
    )

    # try upload with interface
    with capture_on_commit_callbacks(execute=True):
        response = get_view_for_user(
            viewname="api:upload-session-list",
            user=user,
            client=client,
            method=client.post,
            content_type="application/json",
            data={
                "uploads": [upload.api_url],
                "reader_study": rs.slug,
                "interface": "generic-overlay",
            },
            HTTP_X_FORWARDED_PROTO="https",
        )

    assert response.status_code == 400
    assert (
        "An interface can only be defined for archive, archive item or display set uploads."
        in response.json()["non_field_errors"])

    # try without interface
    with capture_on_commit_callbacks(execute=True):
        response = get_view_for_user(
            viewname="api:upload-session-list",
            user=user,
            client=client,
            method=client.post,
            content_type="application/json",
            data={
                "uploads": [upload.api_url],
                "reader_study": rs.slug
            },
            HTTP_X_FORWARDED_PROTO="https",
        )

    assert response.status_code == 201
    upload_session = response.json()
    assert upload_session["uploads"] == [upload.api_url]
Exemple #8
0
def test_upload_some_images(client: Client, challenge_set, settings):
    # Override the celery settings
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    user = UserFactory()

    # Use reader studies as this uses UploadRawImagesForm
    rs = ReaderStudyFactory(use_display_sets=False)
    rs.add_editor(user)

    response = get_view_for_user(
        client=client,
        viewname="reader-studies:add-images",
        user=user,
        reverse_kwargs={"slug": rs.slug},
    )
    assert response.status_code == 200

    assert rs.images.count() == 0
    assert RawImageUploadSession.objects.count() == 0

    user_upload = create_upload_from_file(file_path=RESOURCE_PATH /
                                          "image10x10x10.mha",
                                          creator=user)

    with capture_on_commit_callbacks(execute=True):
        response = get_view_for_user(
            data={"user_uploads": [user_upload.pk]},
            client=client,
            viewname="reader-studies:add-images",
            user=user,
            reverse_kwargs={"slug": rs.slug},
            method=client.post,
        )

    assert response.status_code == 302
    assert rs.images.count() == 1
    sessions = RawImageUploadSession.objects.all()
    assert len(sessions) == 1

    response = get_view_for_user(url=sessions[0].get_absolute_url(),
                                 client=client,
                                 user=user)
    assert response.status_code == 200

    response = get_view_for_user(
        url=sessions[0].get_absolute_url(),
        client=client,
        user=UserFactory(is_staff=True),
    )
    assert response.status_code == 403
Exemple #9
0
def test_assign_answer_image(client, settings):
    settings.task_eager_propagates = (True,)
    settings.task_always_eager = (True,)
    rs = ReaderStudyFactory()
    im = ImageFactory()
    editor, reader = UserFactory(), UserFactory()

    rs.images.add(im)
    rs.add_editor(editor)
    rs.add_reader(reader)

    question = QuestionFactory(
        reader_study=rs, answer_type=Question.ANSWER_TYPE_POLYGON_IMAGE
    )

    us = RawImageUploadSessionFactory(creator=reader)

    answer = AnswerFactory(
        creator=reader,
        question=question,
        answer={"upload_session_pk": str(us.pk)},
    )

    f = StagedFileFactory(
        file__from_path=Path(__file__).parent.parent
        / "cases_tests"
        / "resources"
        / "image10x10x10.mha"
    )
    RawImageFileFactory(upload_session=us, staged_file_id=f.file_id)

    response = get_view_for_user(
        viewname="api:upload-session-process-images",
        reverse_kwargs={"pk": us.pk},
        user=reader,
        client=client,
        method=client.patch,
        data={"answer": str(answer.pk)},
        content_type="application/json",
    )

    assert response.status_code == 200

    answer.refresh_from_db()
    image = us.image_set.first()

    assert answer.answer_image == image
    assert reader.has_perm("view_image", image)
    assert editor.has_perm("view_image", image)
Exemple #10
0
class TwoReaderStudies:
    def __init__(self):
        self.creator = get_rs_creator()
        self.rs1, self.rs2 = ReaderStudyFactory(
            use_display_sets=False), ReaderStudyFactory(use_display_sets=False)
        self.editor1, self.reader1, self.editor2, self.reader2 = (
            UserFactory(),
            UserFactory(),
            UserFactory(),
            UserFactory(),
        )
        self.rs1.add_editor(user=self.editor1)
        self.rs2.add_editor(user=self.editor2)
        self.rs1.add_reader(user=self.reader1)
        self.rs2.add_reader(user=self.reader2)
        self.u = UserFactory()
Exemple #11
0
def test_upload_some_images(client: Client, challenge_set, settings):
    # Override the celery settings
    settings.task_eager_propagates = (True,)
    settings.task_always_eager = (True,)

    user = UserFactory()

    # Use reader studies as this uses UploadRawImagesForm
    rs = ReaderStudyFactory()
    rs.add_editor(user)

    response = get_view_for_user(
        client=client,
        viewname="reader-studies:add-images",
        user=user,
        reverse_kwargs={"slug": rs.slug},
    )
    assert response.status_code == 200

    assert rs.images.count() == 0
    assert RawImageUploadSession.objects.count() == 0

    file1 = create_file_from_filepath(RESOURCE_PATH / "image10x10x10.mha")
    response = get_view_for_user(
        data={"files": f"{file1.uuid}"},
        client=client,
        viewname="reader-studies:add-images",
        user=user,
        reverse_kwargs={"slug": rs.slug},
        method=client.post,
    )
    assert response.status_code == 302
    assert rs.images.count() == 1
    sessions = RawImageUploadSession.objects.all()
    assert len(sessions) == 1

    response = get_view_for_user(
        url=sessions[0].get_absolute_url(), client=client, user=user
    )
    assert response.status_code == 200

    response = get_view_for_user(
        url=sessions[0].get_absolute_url(),
        client=client,
        user=UserFactory(is_staff=True),
    )
    assert response.status_code == 403
def test_assign_score(settings):
    settings.task_eager_propagates = (True,)
    settings.task_always_eager = (True,)

    rs = ReaderStudyFactory()
    im = ImageFactory()
    q1 = QuestionFactory(reader_study=rs)
    q2 = QuestionFactory(
        reader_study=rs, answer_type=Question.ANSWER_TYPE_MULTIPLE_CHOICE
    )
    e, r1, r2 = UserFactory(), UserFactory(), UserFactory()

    rs.images.add(im)
    rs.add_editor(e)
    rs.add_reader(r1)
    rs.add_reader(r2)

    a1 = AnswerFactory(question=q1, creator=r1, answer="foo")
    a1.images.add(im)
    assert a1.score is None

    gt = AnswerFactory(
        question=q1, creator=e, answer="foo", is_ground_truth=True
    )
    gt.images.add(im)
    a1.refresh_from_db()
    assert a1.score == 1.0

    a2 = AnswerFactory(question=q1, creator=r2, answer="foo")
    a2.images.add(im)
    a2.refresh_from_db()
    assert a2.score == 1.0

    a1 = AnswerFactory(question=q2, creator=r1, answer=[])
    a1.images.add(im)
    assert a1.score is None

    gt = AnswerFactory(question=q2, creator=e, answer=[], is_ground_truth=True)
    gt.images.add(im)
    a1.refresh_from_db()
    assert a1.score == 1.0

    a2 = AnswerFactory(question=q2, creator=r2, answer=[])
    a2.images.add(im)
    a2.refresh_from_db()
    assert a2.score == 1.0
def test_generate_hanging_list_api_view(generate_hanging_list, client):
    rs = ReaderStudyFactory()
    editor = UserFactory()
    rs.add_editor(editor)

    response = get_view_for_user(
        viewname="api:reader-study-generate-hanging-list",
        reverse_kwargs={"pk": rs.pk},
        user=editor,
        client=client,
        method=client.patch,
        follow=True,
    )

    assert response.status_code == 200
    assert "Hanging list generated." in str(response.content)
    generate_hanging_list.assert_called_once()
Exemple #14
0
def test_reader_study_delete(client):
    rs = ReaderStudyFactory(use_display_sets=False)
    editor = UserFactory()
    reader = UserFactory()
    rs.add_editor(editor)
    rs.add_reader(reader)

    assert ReaderStudy.objects.count() == 1
    assert is_following(user=editor, obj=rs)

    response = get_view_for_user(
        viewname="reader-studies:delete",
        client=client,
        method=client.get,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        user=reader,
    )

    assert response.status_code == 403
    assert ReaderStudy.objects.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:delete",
        client=client,
        method=client.get,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert "Confirm Deletion" in response.rendered_content

    response = get_view_for_user(
        viewname="reader-studies:delete",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 0
    assert not is_following(user=editor, obj=rs)
def test_api_list_is_filtered(client):
    rs1, rs2 = ReaderStudyFactory(), ReaderStudyFactory()
    rs1_editor = UserFactory()
    rs1.add_editor(rs1_editor)
    q1, q2 = (
        QuestionFactory(reader_study=rs1),
        QuestionFactory(reader_study=rs2),
    )
    a1, _ = (
        AnswerFactory(question=q1, answer=True),
        AnswerFactory(question=q2, answer=False),
    )

    response = get_view_for_user(viewname="api:reader-study-list",
                                 user=rs1_editor,
                                 client=client)
    assert response.status_code == 200
    assert response.json()["count"] == 1

    response = get_view_for_user(
        viewname="api:reader-study-detail",
        reverse_kwargs={"pk": rs1.pk},
        user=rs1_editor,
        client=client,
    )
    assert response.status_code == 200
    assert len(response.json()["questions"]) == 1

    response = get_view_for_user(
        viewname="api:reader-studies-question-list",
        user=rs1_editor,
        client=client,
    )
    assert response.status_code == 200
    assert response.json()["count"] == 1
    assert response.json()["results"][0]["pk"] == str(q1.pk)

    response = get_view_for_user(
        viewname="api:reader-studies-answer-list",
        user=rs1_editor,
        client=client,
    )
    assert response.status_code == 200
    assert response.json()["count"] == 1
    assert response.json()["results"][0]["pk"] == str(a1.pk)
Exemple #16
0
def test_answer_remove(client):
    rs = ReaderStudyFactory()
    r1, r2, editor = UserFactory(), UserFactory(), UserFactory()
    rs.add_reader(r1)
    rs.add_reader(r2)
    rs.add_editor(editor)
    q = QuestionFactory(
        reader_study=rs,
        question_text="q1",
        answer_type=Question.ANSWER_TYPE_BOOL,
    )
    im = ImageFactory()
    a1 = AnswerFactory(creator=r1, question=q, answer=True)
    a1.images.set([im])
    a2 = AnswerFactory(creator=r2, question=q, answer=True)
    a2.images.set([im])
    assert Answer.objects.count() == 2

    response = get_view_for_user(
        viewname="reader-studies:answers-remove",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"user": r1.id},
        follow=True,
        user=r1,
    )

    assert response.status_code == 403

    response = get_view_for_user(
        viewname="reader-studies:answers-remove",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"user": r1.id},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert Answer.objects.count() == 1
    assert Answer.objects.filter(creator=r1).count() == 0
    assert Answer.objects.filter(creator=r2).count() == 1
def test_upload_session_owned_by_answer_creator(client, settings, answer_type):
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    rs = ReaderStudyFactory()
    im = ImageFactory()
    editor, reader = UserFactory(), UserFactory()

    rs.images.add(im)
    rs.add_editor(editor)
    rs.add_reader(reader)

    question = QuestionFactory(reader_study=rs, answer_type=answer_type)

    us1 = RawImageUploadSessionFactory(creator=reader)
    us2 = RawImageUploadSessionFactory(creator=editor)

    answer1 = AnswerFactory(
        creator=reader,
        question=question,
        answer={"upload_session_pk": str(us1.pk)},
    )

    f = StagedFileFactory(file__from_path=Path(__file__).parent.parent /
                          "cases_tests" / "resources" / "image10x10x10.mha")
    RawImageFileFactory(upload_session=us1, staged_file_id=f.file_id)

    response = get_view_for_user(
        viewname="api:upload-session-process-images",
        reverse_kwargs={"pk": us2.pk},
        user=editor,
        client=client,
        method=client.patch,
        data={"answer": str(answer1.pk)},
        content_type="application/json",
    )

    assert response.status_code == 400
    assert (b"User does not have permission to add an image to this answer"
            in response.rendered_content)
Exemple #18
0
def test_question_delete(client):
    rs = ReaderStudyFactory(use_display_sets=False)
    r1, editor = UserFactory(), UserFactory()
    rs.add_reader(r1)
    rs.add_editor(editor)
    q = QuestionFactory(
        reader_study=rs,
        question_text="q1",
        answer_type=Question.AnswerType.BOOL,
    )
    assert Question.objects.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:question-delete",
        client=client,
        method=client.post,
        reverse_kwargs={
            "slug": rs.slug,
            "pk": q.pk
        },
        follow=True,
        user=r1,
    )

    assert response.status_code == 403

    response = get_view_for_user(
        viewname="reader-studies:question-delete",
        client=client,
        method=client.post,
        reverse_kwargs={
            "slug": rs.slug,
            "pk": q.pk
        },
        user=editor,
    )

    assert response.status_code == 302
    assert Question.objects.count() == 0
    assert str(rs) in response.url
def reader_study_with_gt():
    rs = ReaderStudyFactory()
    im1, im2 = ImageFactory(name="im1"), ImageFactory(name="im2")
    q1, q2, q3 = [
        QuestionFactory(
            reader_study=rs,
            answer_type=Question.AnswerType.BOOL,
            question_text="q1",
        ),
        QuestionFactory(
            reader_study=rs,
            answer_type=Question.AnswerType.BOOL,
            question_text="q2",
        ),
        QuestionFactory(
            reader_study=rs,
            answer_type=Question.AnswerType.BOOL,
            question_text="q3",
        ),
    ]

    r1, r2, editor = UserFactory(), UserFactory(), UserFactory()
    rs.add_reader(r1)
    rs.add_reader(r2)
    rs.add_editor(editor)
    rs.images.set([im1, im2])
    rs.hanging_list = [{"main": im1.name}, {"main": im2.name}]
    rs.save()

    for question in [q1, q2, q3]:
        for im in [im1, im2]:
            ans = AnswerFactory(
                question=question,
                creator=editor,
                answer=True,
                is_ground_truth=True,
            )
            ans.images.add(im)

    return rs
Exemple #20
0
def test_reader_study_display_set_list(client):
    user = UserFactory()
    rs = ReaderStudyFactory(use_display_sets=True)
    rs.add_editor(user)

    civ = ComponentInterfaceValueFactory(image=ImageFactory())
    ds = DisplaySetFactory(reader_study=rs)
    ds.values.add(civ)

    response = get_view_for_user(
        viewname="reader-studies:display_sets",
        reverse_kwargs={"slug": rs.slug},
        client=client,
        user=user,
    )

    assert response.status_code == 200

    response = get_view_for_user(
        viewname="reader-studies:display_sets",
        reverse_kwargs={"slug": rs.slug},
        client=client,
        user=user,
        method=client.get,
        follow=True,
        data={
            "length": 10,
            "draw": 1,
            "order[0][dir]": "desc",
            "order[0][column]": 0,
        },
        **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"},
    )

    resp = response.json()
    assert str(ds.pk) in resp["data"][0][0]
def test_csv_export(client, answer_type, answer):
    im = ImageFactory()

    rs = ReaderStudyFactory()
    rs.images.add(im)
    rs.save()

    editor = UserFactory()
    rs.add_editor(editor)

    reader = UserFactory()
    rs.add_reader(reader)

    q = QuestionFactory(question_text="foo",
                        reader_study=rs,
                        answer_type=answer_type)

    a = AnswerFactory(question=q, answer=answer)
    a.images.add(im)
    a.save()

    response = get_view_for_user(
        viewname="api:reader-studies-answer-list",
        params={"question__reader_study": str(rs.pk)},
        user=editor,
        client=client,
        method=client.get,
        HTTP_ACCEPT="text/csv",
    )

    headers = str(response.serialize_headers())
    content = str(response.content)

    assert response.status_code == 200
    assert "Content-Type: text/csv" in headers

    if isinstance(answer, dict):
        for key in answer:
            assert key in content
    else:
        assert re.sub(r"\n", r"\\n", str(a.answer)) in content
    assert a.creator.username in content

    response = get_view_for_user(
        viewname="api:reader-studies-question-list",
        params={"reader_study": str(rs.pk)},
        user=editor,
        client=client,
        method=client.get,
        HTTP_ACCEPT="text/csv",
    )

    headers = str(response.serialize_headers())
    content = str(response.content)

    assert response.status_code == 200
    assert "Content-Type: text/csv" in headers

    assert a.question.question_text in content
    assert a.question.get_answer_type_display() in content
    assert str(a.question.required) in content
    assert a.question.get_image_port_display() in content

    response = get_view_for_user(
        viewname="api:image-list",
        params={"readerstudies": str(rs.pk)},
        user=editor,
        client=client,
        method=client.get,
        HTTP_ACCEPT="text/csv",
    )

    headers = str(response.serialize_headers())
    content = str(response.content)

    assert response.status_code == 200
    assert "Content-Type: text/csv" in headers

    assert im.name in content
def test_progress_for_user(settings):
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    rs = ReaderStudyFactory()
    im1, im2 = ImageFactory(name="im1"), ImageFactory(name="im2")
    q1, q2, q3 = [
        QuestionFactory(reader_study=rs),
        QuestionFactory(reader_study=rs),
        QuestionFactory(reader_study=rs),
    ]

    reader = UserFactory()
    rs.add_reader(reader)

    question_perc = 100 / 6

    assert rs.get_progress_for_user(reader) == {
        "diff": 0.0,
        "hangings": 0.0,
        "questions": 0.0,
    }

    rs.images.set([im1, im2])
    rs.hanging_list = [{"main": im1.name}, {"main": im2.name}]
    rs.save()

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == 0

    a11 = AnswerFactory(question=q1, answer="foo", creator=reader)
    a11.images.add(im1)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == pytest.approx(question_perc)

    a21 = AnswerFactory(question=q1, answer="foo", creator=reader)
    a21.images.add(im2)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == pytest.approx(question_perc * 2)

    a12 = AnswerFactory(question=q2, answer="foo", creator=reader)
    a12.images.add(im1)
    a13 = AnswerFactory(question=q3, answer="foo", creator=reader)
    a13.images.add(im1)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 50
    assert progress["questions"] == pytest.approx(question_perc * 4)

    editor = UserFactory()
    rs.add_reader(editor)
    rs.add_editor(editor)

    for q in [q1, q2, q3]:
        for im in [im1, im2]:
            a = AnswerFactory(question=q,
                              answer="foo",
                              creator=editor,
                              is_ground_truth=True)
            a.images.add(im)

    progress = rs.get_progress_for_user(editor)
    assert progress["hangings"] == 0
    assert progress["questions"] == 0

    for q in [q1, q2, q3]:
        for im in [im1, im2]:
            a = AnswerFactory(question=q,
                              answer="foo",
                              creator=editor,
                              is_ground_truth=False)
            a.images.add(im)

    progress = rs.get_progress_for_user(editor)
    assert progress["hangings"] == 100.0
    assert progress["questions"] == 100.0
def test_answer_update(client):
    im1, im2 = ImageFactory(), ImageFactory()

    rs = ReaderStudyFactory()
    rs.images.add(im1, im2)
    rs.save()

    reader = UserFactory()
    rs.add_reader(reader)

    editor = UserFactory()
    rs.add_editor(editor)

    q = QuestionFactory(reader_study=rs, answer_type=Question.AnswerType.BOOL)

    response = get_view_for_user(
        viewname="api:reader-studies-answer-list",
        user=reader,
        client=client,
        method=client.post,
        data={
            "answer": True,
            "images": [im1.api_url],
            "question": q.api_url
        },
        content_type="application/json",
    )
    assert response.status_code == 201

    answer = Answer.objects.get(pk=response.data.get("pk"))
    assert answer.answer is True
    assert answer.images.first() == im1
    assert answer.history.count() == 1

    response = get_view_for_user(
        viewname="api:reader-studies-answer-detail",
        reverse_kwargs={"pk": answer.pk},
        user=reader,
        client=client,
        method=client.patch,
        data={
            "answer": False,
            "images": [im2.api_url]
        },
        content_type="application/json",
    )
    assert response.status_code == 400

    answer.refresh_from_db()
    assert response.json() == {
        "non_field_errors":
        ["This reader study does not allow answer modification."]
    }
    assert answer.answer is True
    assert answer.images.first() == im1
    assert answer.history.count() == 1

    rs.allow_answer_modification = True
    rs.save()

    response = get_view_for_user(
        viewname="api:reader-studies-answer-detail",
        reverse_kwargs={"pk": answer.pk},
        user=reader,
        client=client,
        method=client.patch,
        data={
            "answer": False,
            "images": [im2.api_url]
        },
        content_type="application/json",
    )
    assert response.status_code == 400

    answer.refresh_from_db()
    assert response.json() == {
        "non_field_errors": ["Only the answer field can be modified."]
    }
    assert answer.answer is True
    assert answer.images.first() == im1
    assert answer.history.count() == 1

    response = get_view_for_user(
        viewname="api:reader-studies-answer-detail",
        reverse_kwargs={"pk": answer.pk},
        user=reader,
        client=client,
        method=client.patch,
        data={"answer": False},
        content_type="application/json",
    )
    assert response.status_code == 200

    answer.refresh_from_db()
    assert answer.answer is False
    assert answer.images.first() == im1
    assert answer.history.count() == 2

    response = get_view_for_user(
        viewname="api:reader-studies-answer-detail",
        reverse_kwargs={"pk": answer.pk},
        user=editor,
        client=client,
        method=client.patch,
        data={"answer": False},
        content_type="application/json",
    )
    assert response.status_code == 403

    answer.refresh_from_db()
    assert answer.answer is False
    assert answer.history.count() == 2
Exemple #24
0
def test_progress_for_user(settings, use_display_sets):  # noqa: C901
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )

    rs = ReaderStudyFactory(use_display_sets=use_display_sets)
    im1, im2 = ImageFactory(name="im1"), ImageFactory(name="im2")
    q1, q2, q3 = [
        QuestionFactory(reader_study=rs),
        QuestionFactory(reader_study=rs),
        QuestionFactory(reader_study=rs),
    ]

    reader = UserFactory()
    rs.add_reader(reader)

    question_perc = 100 / 6

    assert rs.get_progress_for_user(reader) == {
        "diff": 0.0,
        "hangings": 0.0,
        "questions": 0.0,
    }

    if use_display_sets:
        ci = ComponentInterface.objects.get(slug="generic-medical-image")
        civ1 = ComponentInterfaceValueFactory(image=im1, interface=ci)
        civ2 = ComponentInterfaceValueFactory(image=im2, interface=ci)
        ds1, ds2 = DisplaySetFactory(reader_study=rs), DisplaySetFactory(
            reader_study=rs)
        ds1.values.add(civ1)
        ds2.values.add(civ2)

    else:
        rs.images.set([im1, im2])
        rs.hanging_list = [{"main": im1.name}, {"main": im2.name}]
        rs.save()

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == 0

    a11 = AnswerFactory(question=q1, answer="foo", creator=reader)
    if use_display_sets:
        a11.display_set = ds1
        a11.save()
    else:
        a11.images.add(im1)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == pytest.approx(question_perc)

    a21 = AnswerFactory(question=q1, answer="foo", creator=reader)
    if use_display_sets:
        a21.display_set = ds2
        a21.save()
    else:
        a21.images.add(im2)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 0
    assert progress["questions"] == pytest.approx(question_perc * 2)

    a12 = AnswerFactory(question=q2, answer="foo", creator=reader)
    a13 = AnswerFactory(question=q3, answer="foo", creator=reader)
    if use_display_sets:
        a12.display_set = ds1
        a12.save()
        a13.display_set = ds1
        a13.save()
    else:
        a12.images.add(im1)
        a13.images.add(im1)

    progress = rs.get_progress_for_user(reader)
    assert progress["hangings"] == 50
    assert progress["questions"] == pytest.approx(question_perc * 4)

    editor = UserFactory()
    rs.add_reader(editor)
    rs.add_editor(editor)

    for q in [q1, q2, q3]:
        if use_display_sets:
            for ds in [ds1, ds2]:
                a = AnswerFactory(
                    question=q,
                    answer="foo",
                    creator=editor,
                    is_ground_truth=True,
                    display_set=ds,
                )
        else:
            for im in [im1, im2]:
                a = AnswerFactory(
                    question=q,
                    answer="foo",
                    creator=editor,
                    is_ground_truth=True,
                )
                a.images.add(im)

    progress = rs.get_progress_for_user(editor)
    assert progress["hangings"] == 0
    assert progress["questions"] == 0

    for q in [q1, q2, q3]:
        if use_display_sets:
            for ds in [ds1, ds2]:
                a = AnswerFactory(
                    question=q,
                    answer="foo",
                    creator=editor,
                    is_ground_truth=False,
                    display_set=ds,
                )
        else:
            for im in [im1, im2]:
                a = AnswerFactory(
                    question=q,
                    answer="foo",
                    creator=editor,
                    is_ground_truth=False,
                )
                a.images.add(im)

    progress = rs.get_progress_for_user(editor)
    assert progress["hangings"] == 100.0
    assert progress["questions"] == 100.0
Exemple #25
0
def test_archive_items_to_reader_study_update(client, settings):
    settings.task_eager_propagates = (True, )
    settings.task_always_eager = (True, )
    archive = ArchiveFactory()
    rs1 = ReaderStudyFactory(use_display_sets=True)
    rs2 = ReaderStudyFactory(use_display_sets=False)

    editor, user = UserFactory(), UserFactory()
    archive.add_user(user)
    archive.add_editor(editor)
    rs1.add_editor(editor)
    rs2.add_editor(editor)

    im1, im2, im3, im4 = ImageFactory.create_batch(4)
    overlay = ComponentInterface.objects.get(slug="generic-overlay")
    image = ComponentInterface.objects.get(slug="generic-medical-image")

    civ1, civ2, civ3, civ4 = (
        ComponentInterfaceValueFactory(interface=image, image=im1),
        ComponentInterfaceValueFactory(interface=image, image=im2),
        ComponentInterfaceValueFactory(interface=overlay, image=im3),
        ComponentInterfaceValueFactory(interface=overlay, image=im4),
    )

    ai1 = ArchiveItemFactory(archive=archive)
    ai2 = ArchiveItemFactory(archive=archive)

    ai1.values.add(civ1)
    ai2.values.add(civ2)

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        reverse_kwargs={"slug": archive.slug},
        user=user,
    )
    assert response.status_code == 200
    assert str(rs1.pk) not in response.rendered_content
    assert str(rs2.pk) not in response.rendered_content

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert str(rs1.pk) in response.rendered_content
    assert str(rs2.pk) not in response.rendered_content

    assert im1.name in response.rendered_content
    assert im2.name in response.rendered_content
    assert im3.name not in response.rendered_content
    assert im4.name not in response.rendered_content

    ai1.values.add(civ3)
    ai2.values.add(civ4)

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200

    assert f"{im1.name}, {im3.name}" in response.rendered_content
    assert f"{im2.name}, {im4.name}" in response.rendered_content
Exemple #26
0
def test_archive_items_to_reader_study_update_form(client, settings):
    settings.task_eager_propagates = (True,)
    settings.task_always_eager = (True,)
    archive = ArchiveFactory()
    rs1, rs2 = ReaderStudyFactory(use_display_sets=True), ReaderStudyFactory(
        use_display_sets=False
    )

    editor, reader = UserFactory(), UserFactory()
    archive.editors_group.user_set.add(editor)
    rs1.add_editor(editor)
    rs1.add_reader(reader)

    rs2.add_editor(editor)

    im1, im2, im3, im4 = ImageFactory.create_batch(4)
    overlay = ComponentInterface.objects.get(slug="generic-overlay")
    image = ComponentInterface.objects.get(slug="generic-medical-image")

    civ1, civ2, civ3, civ4 = (
        ComponentInterfaceValueFactory(interface=image, image=im1),
        ComponentInterfaceValueFactory(interface=image, image=im2),
        ComponentInterfaceValueFactory(interface=overlay, image=im3),
        ComponentInterfaceValueFactory(interface=overlay, image=im4),
    )

    ai1 = ArchiveItemFactory(archive=archive)
    ai2 = ArchiveItemFactory(archive=archive)

    ai1.values.add(civ1)
    ai2.values.add(civ2)

    assert rs1.display_sets.count() == 0

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        method=client.post,
        data={"items": [ai1.pk, ai2.pk], "reader_study": rs1.pk},
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=reader,
    )

    assert response.status_code == 403
    assert rs1.display_sets.count() == 0

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        method=client.post,
        data={"items": [ai1.pk, ai2.pk], "reader_study": rs1.pk},
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert rs1.display_sets.count() == 2
    assert sorted(
        list(rs1.display_sets.values_list("values", flat=True))
    ) == sorted([civ1.pk, civ2.pk])

    assert rs2.display_sets.count() == 0
    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        method=client.post,
        data={"items": [ai1.pk, ai2.pk], "reader_study": rs2.pk},
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert rs2.display_sets.count() == 0

    ai1.values.add(civ3)
    ai2.values.add(civ4)

    response = get_view_for_user(
        viewname="archives:items-reader-study-update",
        client=client,
        method=client.post,
        data={"items": [ai1.pk, ai2.pk], "reader_study": rs1.pk},
        reverse_kwargs={"slug": archive.slug},
        follow=True,
        user=editor,
    )

    assert response.status_code == 200
    assert rs1.display_sets.count() == 4
    assert sorted(
        sorted(list(ds.values.values_list("pk", flat=True)))
        for ds in rs1.display_sets.all()
    ) == sorted([[civ1.pk], [civ2.pk], [civ1.pk, civ3.pk], [civ2.pk, civ4.pk]])
Exemple #27
0
def test_example_ground_truth(client, tmpdir):
    rs = ReaderStudyFactory()
    reader, editor = UserFactory(), UserFactory()
    q1, q2, q3 = (
        QuestionFactory(
            reader_study=rs,
            question_text="q1",
            answer_type=Question.ANSWER_TYPE_BOOL,
        ),
        QuestionFactory(
            reader_study=rs,
            question_text="q2",
            answer_type=Question.ANSWER_TYPE_CHOICE,
        ),
        QuestionFactory(
            reader_study=rs,
            question_text="q3",
            answer_type=Question.ANSWER_TYPE_SINGLE_LINE_TEXT,
        ),
    )
    CategoricalOptionFactory(question=q2, title="option")
    im1, im2, im3 = (ImageFactory(), ImageFactory(), ImageFactory())
    rs.images.set([im1, im2, im3])
    rs.add_reader(reader)
    rs.add_editor(editor)
    rs.generate_hanging_list()

    response = get_view_for_user(
        viewname="reader-studies:example-ground-truth",
        client=client,
        method=client.get,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        user=reader,
    )
    assert response.status_code == 403

    response = get_view_for_user(
        viewname="reader-studies:example-ground-truth",
        client=client,
        method=client.get,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        user=editor,
    )
    assert response.status_code == 200
    assert Answer.objects.count() == 0

    gt = io.BytesIO()
    gt.write(response.content)
    gt.seek(0)
    response = get_view_for_user(
        viewname="reader-studies:add-ground-truth",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        follow=True,
        data={"ground_truth": gt},
        user=editor,
    )
    assert response.status_code == 200
    assert Answer.objects.count() == rs.images.count() * rs.questions.count()
    for image in [im1, im2, im3]:
        for question in [q1, q2, q3]:
            assert Answer.objects.filter(images=image,
                                         question=question,
                                         is_ground_truth=True).exists()
Exemple #28
0
def test_reader_study_copy(client):
    rs = ReaderStudyFactory(title="copied")
    editor = UserFactory()
    editor2 = UserFactory()
    reader = UserFactory()
    rs.add_reader(reader)
    rs.add_editor(editor)
    rs.add_editor(editor2)
    QuestionFactory(
        reader_study=rs,
        answer_type=Question.AnswerType.BOOL,
        question_text="q1",
    ),
    QuestionFactory(
        reader_study=rs,
        answer_type=Question.AnswerType.BOOL,
        question_text="q2",
    )

    im1, im2 = ImageFactory(), ImageFactory()

    rs.images.set([im1, im2])
    rs.hanging_list = [{"main": im1.name}, {"main": im2.name}]
    rs.case_text = {im1.name: "test", im2.name: "test2"}
    rs.save()

    assert ReaderStudy.objects.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "1"},
        user=reader,
        follow=True,
    )

    assert response.status_code == 403
    assert ReaderStudy.objects.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "1"},
        user=editor,
        follow=True,
    )

    assert response.status_code == 403
    assert ReaderStudy.objects.count() == 1

    add_perm = Permission.objects.get(
        codename=f"add_{ReaderStudy._meta.model_name}"
    )
    editor.user_permissions.add(add_perm)

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "1"},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 2

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "1"
    assert _rs.images.count() == 0
    assert _rs.questions.count() == 0
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 1
    assert _rs.hanging_list == []
    assert _rs.case_text == {}

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "2", "copy_questions": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 3

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "2"
    assert _rs.questions.count() == 2
    assert _rs.images.count() == 0
    assert _rs.hanging_list == []
    assert _rs.case_text == {}
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "3", "copy_images": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 4

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "3"
    assert _rs.questions.count() == 0
    assert _rs.images.count() == 2
    assert _rs.hanging_list == []
    assert _rs.case_text == {}
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "4", "copy_hanging_list": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert (
        "Hanging list and case text can only be copied if the images are copied as well"
        in response.rendered_content
    )
    assert ReaderStudy.objects.count() == 4

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "4", "copy_images": True, "copy_hanging_list": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 5

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "4"
    assert _rs.questions.count() == 0
    assert _rs.images.count() == 2
    assert _rs.hanging_list == rs.hanging_list
    assert _rs.case_text == {}
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "5", "copy_images": True, "copy_case_text": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 6

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "5"
    assert _rs.questions.count() == 0
    assert _rs.images.count() == 2
    assert _rs.hanging_list == []
    assert _rs.case_text == rs.case_text
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "6", "copy_readers": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 7

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "6"
    assert _rs.questions.count() == 0
    assert _rs.images.count() == 0
    assert _rs.hanging_list == []
    assert _rs.case_text == {}
    assert _rs.readers_group.user_set.count() == 1
    assert _rs.editors_group.user_set.count() == 1

    response = get_view_for_user(
        viewname="reader-studies:copy",
        client=client,
        method=client.post,
        reverse_kwargs={"slug": rs.slug},
        data={"title": "7", "copy_editors": True},
        user=editor,
        follow=True,
    )

    assert response.status_code == 200
    assert ReaderStudy.objects.count() == 8

    _rs = ReaderStudy.objects.order_by("created").last()
    assert _rs.title == "7"
    assert _rs.questions.count() == 0
    assert _rs.images.count() == 0
    assert _rs.hanging_list == []
    assert _rs.case_text == {}
    assert _rs.readers_group.user_set.count() == 0
    assert _rs.editors_group.user_set.count() == 2