3

I use Django 1.8.4 with Python 3.4

I have a model for tournaments that defines a method which returns a string if a subscription is forbidden.

class Tournament(models.Model):
    name = models.CharField(max_length=200, null=True, blank=True)
    subscriptions = models.ManyToManyField('ap_users.Profile')
    is_subscription_open = models.BooleanField(default=True)
    # ...

    def why_subscription_impossible(self, request):
        if not request.user.profile.is_profile_complete():
            return 'Your profile is not complete'
        elif not self.is_subscription_open:
            return 'Subscriptions are closed'
        elif <another_condition>:
            return 'Another error message'

        return None

I want to display the list of tournaments, using a generic ListView, and I want to use the result of the method to modify the way it is displayed:

<table class="table">
    <thead>
        <td>Tournament</td>
        <td>Subscription</td>
    </thead>
    {% for tournament in tournament_list %}
        <tr>
            <td>{{ tournament.name }}</td>
            <td>
                {% if tournament.why_subscription_impossible %}
                    {{ tournament.why_subscription_impossible }}
                {% else %}
                    <a href="{% url 'ap_tournament:subscribe' tournament.id %}">Subscribe</a>
                {% endif %}
            </td>
        </tr>
    {% endfor %}
</table>

The view is a class based generic view inherited from generic.ListView.

class IndexView(generic.ListView):
    template_name = 'ap_tournament/index.html'

    def get_queryset(self):
        return Tournament.objects.all()

The shown solution doesn't work, because I need to pass the current request, to get information about logged user. So I tried to add the result of the method to a context in the view

class IndexView(generic.ListView):
    template_name = 'ap_tournament/index.html'

    def get_queryset(self):
        return Tournament.objects.all()

    def get_context_data(self, **kwargs):
        context = super(IndexView, self).get_context_data(**kwargs)

        additional_ctx_info = []
        for tournament in self.get_queryset():
            additional_ctx_info.append({
                'reason_to_not_subscribe': tournament.why_subscription_impossible(self.request)
            })

        context['subscr_info'] = additional_ctx_info
        return context

Obviously, this doesn't work too. I don't know how to access to the subscr_info[n] with n the current index in the tournament_list. I know the forloop.counter0 to get the index, but I can't use it in the template (or I don't know how). I tried :

  • {{ subscr_info.forloop.counter0.reason_to_not_subscribe }}
  • {{ subscr_info.{{forloop.counter0}}.reason_to_not_subscribe }}

I also tried to annotate the QuerySet in get_queryset() view method and read about aggregate(), but I feel that works only with operations supported by the database (AVG, COUNT, MAX, etc.).

I also feels that using a filter or a template tag will not work in my case since I need to use the result of the method in a if tag.

Is there a better solution or a completely diffferent method to achieve what I want ?

Antwane
  • 20,760
  • 7
  • 51
  • 84
  • I think the method `why_subscription_impossible` should sit somewhere else, but not in the model. The model methods should do something with the object's attributes (and should be specific to that object/row, otherwise for multiple rows put it in a model manager) – Pynchia Sep 20 '15 at 20:44
  • [here](https://docs.djangoproject.com/en/1.8/ref/models/querysets/#annotate) it says that from django 1.8 it is now possible to use any kind of expression in `annotate`. So, to avoid making more than one query (fetching the queryset in get_context_data), move your method outside any class (e.g. to your view.py) and in get_queryset do `return Tournament.objects.annotate(reason=why_subscription_impossible(self.request))`. Try to see if it works, it would be the cleanest solution. – Pynchia Sep 20 '15 at 21:13
  • The example was simplified in this question, but the method actually perform many checks, including on tournament's fields. For that reason, I can move it somewhere else (and adds a tournament parameter to the function) but I can't use this new function as annotation (because I can't pass a different tournament object on each call). I updated the code snippets. – Antwane Sep 21 '15 at 06:55

2 Answers2

1

In your view, you could also do:

tournaments = self.get_queryset()
for tournament in tournaments:
    tournament.reason_to_not_subscribe = tournament.why_subscription_impossible(self.request)

Then add tournaments to the context.

Jed Fox
  • 2,979
  • 5
  • 28
  • 38
  • Yes, I will keep this solution in last chance, but it seems a bit inelegant to me. I will wait a few hours to see if somebody has another solution. – Antwane Sep 21 '15 at 06:57
  • After all, your solution is the only one that worked. accepted – Antwane Sep 22 '15 at 08:33
0

You have to create tournament_list in your views.py file, and set it depending on whether the user is logged and has the corresponding permissions.

If you need to count something, you can create the following Counter class :

class Counter:
    count = 0

    def increment(self):
        self.count += 1
        return ''

    def decrement(self):
        self.count -= 1
        return ''

You can then use it in your templates by calling {{ counter.increment }} and {{ counter.count }}. ({{ subscr_info.{{counter.count}}.reason_to_not_subscribe }} and don't forget to place {{ counter.increment }} in your loop.

However, a nicer workaround I used was to create a dictionnary containing both the main element and the additional information, i.e.

ctx_info = []
for tournament in self.get_queryset():
    ctx_info.append({
        'tournament': tournament
        'reason_to_not_subscribe': tournament.why_subscription_impossible(self.request)
    })

and then loop on ctx_info. It's cleaner, but I however do not know how this can be implemented within a ListView (which I never used)

By the way, your template contains {{ why_subscription_impossible }} instead of {{ tournament.why_subscription_impossible }}, I don't know if it was intended...

Ten
  • 1,283
  • 9
  • 12
  • I don't need your Counter class here, because builtin {{ forloop.counter }} and {{ forloop.counter0 }} do the same thing. I updated the question to fix the error you mentioned in my template. Thanks! I will try your solution soon to check if it can help me. – Antwane Sep 21 '15 at 07:03
  • `{{ subscr_info.{{counter.count}}.reason_to_not_subscribe }}` is not a valid syntax (http://stackoverflow.com/questions/7569133/django-template-for-loop-member-before/11784863#11784863). – Antwane Sep 21 '15 at 07:30