Search code examples
pythondjangopytest

How can I follow an HTTP redirect?


I have 2 different views that seem to work on their own. But when I try to use them together with a http redirect then that fails.

The context is pretty straightforward, I have a view that creates an object and another view that updates this object, both with the same form.

The only thing that is a bit different is that we use multiple sites. So we check if the site that wants to update the object is the site that created it. If yes then it does a normal update of the object. If no (that's the part that does not work here) then I http redirect the update view to the create view and I pass along the object so the new site can create a new object based on those initial values.

Here is the test to create a new resource (passes successfully) :

@pytest.mark.resource_create
@pytest.mark.django_db
def test_create_new_resource_and_redirect(client):
    data = {
        "title": "a title",
        "subtitle": "a sub title",
        "status": 0,
        "summary": "a summary",
        "tags": "#tag",
        "content": "this is some updated content",
    }

    with login(client, groups=["example_com_staff"]):
        response = client.post(reverse("resources-resource-create"), data=data)
    resource = models.Resource.on_site.all()[0]
    assert resource.content == data["content"]
    assert response.status_code == 302

Here is the test to create a new resource from an existing object (passes successfully) :

@pytest.mark.resource_create
@pytest.mark.django_db
def test_create_new_resource_from_pushed_resource_and_redirect(request, client):
    existing_resource = baker.make(models.Resource)

    other_site = baker.make(Site)
    existing_resource.site_origin = other_site
    existing_resource.sites.add(other_site)

    our_site = get_current_site(request)
    existing_resource.sites.add(our_site)

    original_content = "this is some original content"
    existing_resource.content = original_content

    existing_resource.save()

    data = {
        "title": "a title",
        "subtitle": "a sub title",
        "status": 0,
        "summary": "a summary",
        "tags": "#tag",
        "content": "this is some updated content",
    }

    url = reverse("resources-resource-create-from-shared", args=[existing_resource.id])
    with login(client, groups=["example_com_staff"]):
        response = client.post(url, data=data)

    assert response.status_code == 302

    existing_resource.refresh_from_db()
    assert existing_resource.content == original_content
    assert our_site not in existing_resource.sites.all()

    new_resource = models.Resource.on_site.get()
    assert new_resource.content == data["content"]

Here is the create view :

@login_required
def resource_create(request, pushed_resource_id=None):
    """
    Create new resource

    In case of a resource that is pushed from a different site
    create a new resource based on the pushed one.
    """
    has_perm_or_403(request.user, "sites.manage_resources", request.site)

    try:
        pushed_resource = models.Resource.objects.get(id=pushed_resource_id)
        pushed_resource_as_dict = model_to_dict(pushed_resource)
        initial_data = pushed_resource_as_dict
    except ObjectDoesNotExist:
        pushed_resource = None
        initial_data = None

    if request.method == "POST":
        form = EditResourceForm(request.POST, initial=initial_data)
        if form.is_valid():
            resource = form.save(commit=False)
            resource.created_by = request.user
            with reversion.create_revision():
                reversion.set_user(request.user)
                resource.save()
                resource.sites.add(request.site)
                if pushed_resource:
                    pushed_resource.sites.remove(request.site)
                    pushed_resource.save()
                    resource.site_origin = request.site
                    resource.save()
                form.save_m2m()

            next_url = reverse("resources-resource-detail", args=[resource.id])
            return redirect(next_url)
    else:
        form = EditResourceForm()
    return render(request, "resources/resource/create.html", locals())

Here is the test to update the resource from the original site (passes successfully) :

@pytest.mark.resource_update
@pytest.mark.django_db
def test_update_resource_from_origin_site_and_redirect(request, client):
    resource = baker.make(models.Resource)
    our_site = get_current_site(request)
    resource.site_origin = our_site
    resource.save()

    previous_update = resource.updated_on

    url = reverse("resources-resource-update", args=[resource.id])

    data = {
        "title": "a title",
        "subtitle": "a sub title",
        "status": 0,
        "summary": "a summary",
        "tags": "#tag",
        "content": "this is some updated content",
    }

    with login(client, groups=["example_com_staff"]):
        response = client.post(url, data=data)

    assert response.status_code == 302

    resource.refresh_from_db()
    assert resource.content == data["content"]
    assert resource.updated_on > previous_update

And finally the test to update from a different site that should create a new resource from the original one (that one fails):

@pytest.mark.resource_update
@pytest.mark.django_db
def test_update_resource_from_non_origin_site_and_redirect(request, client):
    original_resource = baker.make(models.Resource)
    our_site = get_current_site(request)
    other_site = baker.make(Site)
    original_resource.sites.add(our_site, other_site)
    original_resource.site_origin = other_site
    previous_update = original_resource.updated_on
    original_content = "this is some original content"
    original_resource.content = original_content

    original_resource.save()

    assert models.Resource.on_site.all().count() == 1

    url = reverse("resources-resource-update", args=[original_resource.id])

    updated_data = {
        "title": "a title",
        "subtitle": "a sub title",
        "status": 0,
        "summary": "a summary",
        "tags": "#tag",
        "content": "this is some updated content",
    }

    with login(client, groups=["example_com_staff"]):
        response = client.post(url, data=updated_data)

    assert response.status_code == 302

    original_resource.refresh_from_db()
    assert original_resource.content == original_content
    assert original_resource.updated_on == previous_update
    assert other_site in original_resource.sites.all()
    assert our_site not in original_resource.sites.all()

    assert models.Resource.on_site.all().count() == 1
    new_resource = models.Resource.on_site.get()
    assert new_resource.content == updated_data["content"]
    assert other_site not in new_resource.sites.all()
    assert our_site in new_resource.sites.all()

What happens is that no new object gets created here and the original object is modified instead.

Here is the update view :

@login_required
def resource_update(request, resource_id=None):
    """Update informations for resource"""
    has_perm_or_403(request.user, "sites.manage_resources", request.site)

    resource = get_object_or_404(models.Resource, pk=resource_id)

    if resource.site_origin is not None and resource.site_origin != request.site:
        pushed_resource_id = resource.id
        next_url = reverse("resources-resource-create-from-shared",
                           args=[pushed_resource_id]
                           )
        return redirect(next_url)

    next_url = reverse("resources-resource-detail", args=[resource.id])
    if request.method == "POST":
        form = EditResourceForm(request.POST, instance=resource)
        if form.is_valid():
            resource = form.save(commit=False)
            resource.updated_on = timezone.now()

            with reversion.create_revision():
                reversion.set_user(request.user)
                resource.save()
                form.save_m2m()

            return redirect(next_url)
    else:
        form = EditResourceForm(instance=resource)
    return render(request, "resources/resource/update.html", locals())

And the model form :

class EditResourceForm(forms.ModelForm):
    """Create and update form for resources"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Queryset needs to be here since on_site is dynamic and form is read too soon
        self.fields["category"] = forms.ModelChoiceField(
            queryset=models.Category.on_site.all(),
            empty_label="(Aucune)",
            required=False,
        )

        self.fields["contacts"] = forms.ModelMultipleChoiceField(
            queryset=addressbook_models.Contact.on_site.all(),
            required=False,
        )

        # Try to load the Markdown template into 'content' field
        try:
            tmpl = get_template(
                template_name="resources/resource/create_md_template.md"
            )
            self.fields["content"].initial = tmpl.render()
        except TemplateDoesNotExist:
            pass

    content = MarkdownxFormField(label="Contenu")

    title = forms.CharField(
        label="Titre", widget=forms.TextInput(attrs={"class": "form-control"})
    )
    subtitle = forms.CharField(
        label="Sous-Titre",
        widget=forms.TextInput(attrs={"class": "form-control"}),
        required=False,
    )
    summary = forms.CharField(
        label="Résumé bref",
        widget=forms.Textarea(
            attrs={"class": "form-control", "rows": "3", "maxlength": 400}
        ),
        required=False,
    )

    class Meta:
        model = models.Resource
        fields = [
            "title",
            "status",
            "subtitle",
            "summary",
            "tags",
            "category",
            "departments",
            "content",
            "contacts",
            "expires_on",
        ]

Any idea about what I did wrong is welcome. And if you think a better strategy should be employed then feel free to comment.


Solution

  • My bad. I use initial=initial_data in the POST part of the create view. Which makes no sense.

    When moving the initial=initial_data to the GET part then it works.

    The test_update_resource_from_non_origin_site_and_redirect test still fails though. I'm going to investigate since the feature works fine from within the web interface.