czue / celery-progress

Drop in, configurable, dependency-free progress bars for your Django/Celery applications.
MIT License
467 stars 90 forks source link

Redirect to django url on task complete #41

Closed j3dd3rs closed 4 years ago

j3dd3rs commented 4 years ago

I did see someone did ask about doing a redirect on a task complete and had no error/was successful. I am after something similar but looking to redirect to a url from my urls.py not too familiar with javascript so trying to figure out exactly how I would go about this. I have got my template setup to run the progress bar just fine. But then my goal would be to after 'x' amount of seconds (undecided yet) to redirect to a url as an example for the current work I am looking to redirect to this url:

path('search/<int:pk>', views.search_list, name='search_list')

For now I am struggling as the view the progress bar is being called from wil be using the return render:

return render(request, 'prop/search_multiple.html', {'formset': formset,'task_id': task_id})

In the template I have the progress bar:


{% if task_id %}
        <script type="text/javascript">
            // Progress Bar (JQuery)
            $(function () {
                var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
                CeleryProgressBar.initProgressBar(progressUrl, {})
            });
        </script>
    {% endif %}

Would I be adding something into to do with onsuccess? Or would I add something elsewhere? I really need to get around to learning some javascript so I have have a go at these myself but I thought asking on here could be a good way about it. I am not exactly sure if this is the place I should be posting this sort of question either as a bit new to how people use Github and if this is generally used more for actual bugs in the code or suggestions for it.

eeintech commented 4 years ago

@j3dd3rs Not your Javascript expert here but what about something like this:

{% if task_id %}
    <script type="text/javascript">
        function processSuccess(progressBarElement, progressBarMessageElement, result) {
            // Build Success URL (replace 'namespace' with your own)
            var success_url = "{% url 'namespace:search_list' task_id %}"
            // Redirect to URL
            window.location.replace(success_url)
        }

        // Progress Bar (JQuery)
        $(function () {
            var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
            CeleryProgressBar.initProgressBar(progressUrl, {
                onSuccess: processSuccess,
            })
        });
    </script>
{% endif %}
j3dd3rs commented 4 years ago

Thanks! Now I have another issue sort of linked to the first as I am trying to pass a model id which is created in my task to the redirect. I am struggling to receive a return value from the task and get the progress bar (and redirect) to work. It ends in some infinite loop and I am not quite sure what I am doing wrong.

search_task = search_multiple_task.delay(compound_name_list, current_user.id)
task_id = search_task.task_id
return render(request, 'prop/search_multiple.html', {'formset': formset,'task_id': task_id, 'redirect_id': search_task.get()})

It just ends up repeating the GET for the task_id [05/Aug/2020 23:53:41] "GET /celery-progress/306a9a7b-b7e9-4de3-ad4c-b8c2b65998b1/ HTTP/1.1" 200 92

Am I missing something on how I retrieve the value returned in the task at the very end of the task and use the progress bar? I've had a look around and nothing seems to be coming up.

eeintech commented 4 years ago

What don't you try to split things up as follow:

Without seeing the entire code, I'm not quite sure I picked up the right examples but I hope this helps.

j3dd3rs commented 4 years ago

Struggling to get this to work even with a url that doesn't require any parameters to be passed into the url so not quire sure what is going wrong. It just finishes the progress bar and then never redirects to any other url and stays on the same page.

I'll post a few more bits of code to maybe try help get a better idea of how this would be put together.

It is placed within an app called prop.

urls.py

path('search_multiple/', views.search_multiple, name='search-multiple'),
path('search/<int:pk>', views.search_list, name='search_list'),

views.py

def search_multiple(request):
    if request.method == 'GET':
        formset = CompoundFormset(request.GET or None)
    elif request.method == 'POST':
        current_user = request.user
        formset = CompoundFormset(request.POST)
        if formset.is_valid():
            compound_name_list = []
            for form in formset:
                compound_name_list.append(form.cleaned_data['compound_search'])
            search_task = search_multiple_task.delay(compound_name_list, current_user.id)
            task_id = search_task.task_id
            return render(request, 'prop/search_multiple.html', {'formset': formset,'task_id': task_id})

    return render(request, 'prop/search_multiple.html', {'formset': formset})

def search_list(request, pk):
    searches = UserSearch.objects.filter(pk=pk).values_list('inchi_key_list', flat=True)
    for keys in searches:
        inchi_keys = keys.split(', ')

    compounds = CompoundProperties.objects.filter(inchi_key__in=inchi_keys)

    paginator = Paginator(compounds, 9)
    page = request.GET.get('page')
    compounds = paginator.get_page(page)

    return render(request, 'prop/results.html', {'properties': compounds})

tasks.py

@shared_task(bind=True)
def search_multiple_task(self, compounds_list, user_id):
    print('Task Started')
    progress_recorder = ProgressRecorder(self)
    length_of_list = len(compounds_list)
    compound_count = 0
    current_user = User.objects.get(pk=user_id)
    print('Start')
    compound_to_process = []
    inchi_key_to_process = []
    image_location_to_process = []

    for compound in compounds_list:
        inchi_key = api_searcher.convert_entry_to_inchikey(compound)

        data = api_searcher.request_compound(inchi_key,
                                             os.path.join('prop/static/compound_images'), compound)

        compound_to_process.append(data['compound_name'])
        inchi_key_to_process.append(inchi_key)
        image_location_to_process.append(data['image_location'])

        model = CompoundProperties(**data)
        model.save()

        progress_recorder.set_progress(compound_count + 1, length_of_list, description='Processing')
        compound_count += 1

    compounds = ', '.join(compound_to_process)
    inchi_key_list = ', '.join(inchi_key_to_process)
    image_location_list = ', '.join(image_location_to_process)

    search_model = UserSearch(user=current_user, inchi_key_list=inchi_key_list, compound_list=compounds,
                              image_location_list=image_location_list)
    search_model.save()
    return search_model.id

This task basically is getting a list of compounds, the api_searcher gets some information for each and it is saved into one of my models. The search_model is a list of the searches which is the one I want to get the pk for to pass into the url to get all of the data for each compound in that search. So that is the important model as that is the pk I want to pass into the search_list url path ultimately.

search_multiple.html

{% extends "prop/base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block content %}
    <form id="my-form" method="post" action="">
        {% csrf_token %}
        {{ formset.media }}
        {{ formset.management_form }}
        {% for form in formset %}
            <div class="individual-form">
                {{ form|crispy }}
            </div>
        {% endfor %}
        <div class="row spacer">
            <div class="col-4 mx-auto">
                <button type="submit" class="btn btn-block btn-primary">Create</button>
            </div>
        </div>
    </form>

    <script src="{% static "dynamic_formsets/jquery.formset.js" %}" type="text/javascript"></script>

    <script type="text/javascript">
        $('.individual-form').formset();
    </script>

    <!-- JQuery -->
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"
            integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <!-- Bootstrap JS -->
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
    <!-- Celery Progress -->
    <script src="{% static 'celery_progress/celery_progress.js' %}"></script>

    <div class="content-section mt-4">
        <div class=" text-center" style="font-size: 14px">
            <div id="progress-bar-message">Waiting for task to start</div>
        </div>
        <div class='progress-wrapper' style="padding-top: 10px;">
            <div id='progress-bar' class='progress-bar progress-bar-striped' role='progressbar'
                 style="height:30px; width: 0%; border-radius: 5px">&nbsp;
            </div>
        </div>
    </div>

    {% if task_id %}
        <script type="text/javascript">
            // Progress Bar (JQuery)
            $(function () {
                var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
                CeleryProgressBar.initProgressBar(progressUrl, {})
            });

            function processSuccess(progressBarElement, progressBarMessageElement, result) {
                // Assuming result contains the model ID
                var model_id = result;
                // Build Success URL (replace 'namespace' with your own)
                var success_url = "{% url 'recent_searches' result %}";
                // Redirect to URL
                window.location.replace(success_url)
            }
        </script>
    {% endif %}

{% endblock content %}

If any of my code needs explaining for help, let me know. It may be a bit all over the place due to being done over a few months and being relatively knew to Python and even newer to Django itself.

eeintech commented 4 years ago

@j3dd3rs I think you forgot to call the processSuccess function in your search_multiple.html template, this bit:

            // Progress Bar (JQuery)
            $(function () {
                var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
                CeleryProgressBar.initProgressBar(progressUrl, {})
            });

should be:

            // Progress Bar (JQuery)
            $(function () {
                var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
                CeleryProgressBar.initProgressBar(progressUrl, {
                    onSuccess: processSuccess,
                })
            });

See the specified onSuccess argument inside the initProgressBar? Without it, the processSuccess function would never be called, hence no redirect. Hopefully, that solves it ;)

Edit: One more thing, you may need to place the processSuccess function before CeleryProgressBar.initProgressBar

j3dd3rs commented 4 years ago

You are right with me forgetting that. Got it to redirect now to a url that doesn't have a parameter so thanks for that solution!

I think this is my last issue. Now I cannot actually get it to pass the model_id into the url. It won't actual give a value into it at all. It just gives a NoReverseMatch error due to nothing being passed in as a parameter:

Exception Type: | NoReverseMatch
-- | --
Reverse for 'search_list' with keyword arguments '{'pk': ''}' not found. 1 pattern(s) tried: ['properties/search/(?P<pk>[0-9]+)$']

I updated the javascript section to be

 {% if task_id %}
        <script type="text/javascript">
            function processSuccess(progressBarElement, progressBarMessageElement, result) {
                // Assuming result contains the model ID
                var model_id = result
                // Build Success URL (replace 'namespace' with your own)
                var success_url = "{% url 'search_list' pk=model_id %}"
                // Redirect to URL
                window.location.replace(success_url)
            }
            // Progress Bar (JQuery)
            $(function () {
                var progressUrl = "{% url 'celery_progress:task_status' task_id %}";
                CeleryProgressBar.initProgressBar(progressUrl, {
                    onSuccess: processSuccess,
                })
            });
        </script>
    {% endif %}

Am I right the result bit should just be the return value from my task which is this bit? I am not sure if I am just missing something somewhere on it all as I cannot really figure out where that result value is being taken from exactly.

    search_model = UserSearch(user=current_user, inchi_key_list=inchi_key_list, compound_list=compounds,
                              image_location_list=image_location_list)
    search_model.save()
    return search_model.id
eeintech commented 4 years ago

Can you print search_model.id before task returns? You can also print the content of result to see if it looks correct, in JS: console.log(result) Also, I don't recall if it keeps the variable type from the task return, check if you need to convert it to integer (if not empty).

j3dd3rs commented 4 years ago

I can print it. When logged it matches the printed result and comes as a number type.

It is happening right as I open the page though when it wouldn't have a variable to send. But surely with it being in the if statement it shouldn't be causing any issues?

eeintech commented 4 years ago

Sorry but the NoReverseMatch error still makes me think it is passed as a string type to var success_url = "{% url 'search_list' pk=model_id %}"

Have you tried changing the type of the expected argument in your views.py to string instead?

path('search/<str:pk>', views.search_list, name='search_list'),
j3dd3rs commented 4 years ago

Okay so it wasn't a conversion issue, it is keeping it as a number variable. I found out this wasn't the issue! I did a bit of searching around when getting back from work and found out it is due to the way Django uses the code compared to Javascript. Django will execute code server side before the client sees it. The Django tags are counted as Django code. So it executes with no parameter and see there is no url to direct to in the templatetag as soon as there is a task_id when you press the submit button. So you need to assign something to replace in the parameter you want to pass in or it sees just an empty value and then breaks.

I used this suggestion I found in this Stackoverflow post (https://stackoverflow.com/questions/17832194/get-javascript-variables-value-in-django-url-template-tag). Basically using this:

var url_mask = "{% url 'someview' arg1=12345 %}".replace(/12345/, tmp.toString());

But adapting it to this for my code instead:

var success_url = "{% url 'search_list' pk=123456789 %}".replace(/123456789/, model_id);

The number is so large for my project I doubt it will ever handle that many ids. It isn't an ideal solution but for my small scale work it will work just fine. Was a good learning experience and thanks so much @eeintech for the help with getting me practically all the way to the working solution.

eeintech commented 4 years ago

@j3dd3rs Haaa yes of course! Great find, I completely skipped the fact the success URL / JS code was evaluated when task_id is passed to the template ({% if task_id %} line). A good lesson for me too :smile:

If you think your issue is resolved, would you mind closing this ticket? Don't hesitate to re-open one in the future :wink:

Happy coding!