Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Request] Document customizing the class based views #55

Open
morenoh149 opened this issue May 29, 2020 · 37 comments
Open

[Request] Document customizing the class based views #55

morenoh149 opened this issue May 29, 2020 · 37 comments
Labels

Comments

@morenoh149
Copy link

The create view is described at https://github.com/pinax/pinax-messages#create-message---code

I would like to do some extra logic here. How can I customize the create view?

@tarsil
Copy link

tarsil commented Jun 1, 2020

@morenoh149 you can always inherit from the model and override the functionality to be the one you prefer or optionally you can always use a proxy model. Something like this:

class MyCustomModel(Message):
    class Meta:
        proxy = True

    def new_message(...):
        # overrides here

@reedjones
Copy link

This seems to be a problem with the whole app, that you can easily customize the templates, but not the logic in the views.... :/

@tarsil
Copy link

tarsil commented Aug 6, 2020

@reedjones that is odd, have you tried to also inherit from the views? Is the same as overriding the models but using the super() to actually apply it. It would be easier if you could provide some examples :-)

@reedjones
Copy link

@tiagoarasilva yeah that might be my problem that I'm not really familiar with class based views. maybe in the documentation could add an example of how to customize the views? There is a reference on customizing the templates (which is easy) but there is no info on how to customize the logic in the views. For example I need to paginate the messages in the inbox views, I need to check if a user is blocked before sending a message, I need to check the content of a message if it has some banned content (like external links for example) before sending the message, etc.... Does anybody have some concrete examples of how to add custom logic to the views?

@tarsil
Copy link

tarsil commented Aug 6, 2020

@reedjones I think I can provide you a few examples of how to work with class-based views but bare in mind that I don't work for pinax :-). I'm just a software (python more specifically) engineer that is trying to help a fellow developer 😄

Ok, let's go by phases. Based on what you just said, I assume you work a lot with function-based views, like this:

def home(request):
    if request.method == 'GET':
        # do something
    elif request.methods == 'POST':
        # another something

Is my assumption correct? Let's transform this into a class-based view

from django.views.generic import TemplateView # You also have FormView, CreateView, UpdateView, ListView ....


class HomeView(TemplateView):
    template_name= 'home.html' # location of yout HTML file in the same way as the render_template in the functions

    def get(self, request, *args, **kwargs): # Also a default inherited from every template view of django
        if not request.user.is_authenticated:
                # DO SOMETHING LIKE RETURNING A REDIRECT OR RAISE A 404
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # DO THE POST LOGIC HERE
        return super().post(request, *args, **kwargs)

This is a small example and is only used for explaining how we can achieve both function and class-based views 😄

Lets override using pinax-message specific views (example purposes)

Ok, using the example of the Django templates from pinax-messages (the Django Rest Framework is the same thing, actually, I've implemented DRF on top of pinax-messages, which means that I'm not even using the template system provided by them) and what you want to achieve:

"For example I need to paginate the messages in the inbox views, I need to check if a user is blocked before sending a message, I need to check the content of a message if it has some banned content (like external links for example) before sending the message, etc.... Does anybody have some concrete examples of how to add custom logic to the views?"

Let's try to demystify this and using an example where you can adapt and do for you own needs.

from django.views.generic import ListView # This is where the pagination magic will happen
from pinax.messages.views import InboxView
from django.core.paginator import Paginator
from django.contrib.auth.mixins import LoginRequiredMixin
from pinax.messages.models import Thread


class MyInboxListView(LoginRequiredMixin, InboxView, ListView):
    """
    We inherit from the mixin LoginRequiredMixin that does the same as the decorator login_required.
    Also, we would like to use the InboxView just for the sake of this example and inherit the context_data already there.
    In the end we apply that to a ListView to paginate
    """
    template_name = 'inbox.html' # location of your inbox html page
    paginate_by = 10 # OR WHATEVER NUMBER YOU DESIRE PER PAGE
    pagination_class = Paginator # OR NumberDetailPagination

    def get_queryset(self):
        # Django ListView by default needs you to implement a queryset in order to apply pagination properly
        # We are assuming you are using pinax-messages models, if not, you just need to apply to your models.
        return Thread.inbox(self.request.user)

This, in theory, return all the inbox messages for the logged in user and that inbox is a classmethod already implemented. Your template will have also:

 def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.kwargs.get("deleted", None):
            threads = Thread.ordered(Thread.deleted(self.request.user))
            folder = "deleted"
        else:
            threads = Thread.ordered(Thread.inbox(self.request.user))
            folder = "inbox"

        context.update({
            "folder": folder,
            "threads": threads,
            "threads_unread": Thread.ordered(Thread.unread(self.request.user))
        })
        return context

Automatically and the reason for it is because we are inheriting the context data but we can add more details to our view. How? Inside our MyInboxListView

def get_context_data(self, *args, **kwargs):
    """
    We will be calling context = super().get_context_data(*args, **kwargs) because we want to get the inherited context data from
    inherited views + our own logic
    """
    context = super().get_context_data(*args, **kwargs) # here is where your confusion is.
    context.update({
        'my_new_details_stuff': 'Value added on my MyInboxListView level view, only!'
    })
    return context

Now, we want to send messages and run validations to understand if the user is blocked or not and all of that, so in your views:

from django.views.generic import ListView # This is where the pagination magic will happen
from django.views.generic import TemplateView
from pinax.messages.views import InboxView, MessageCreateView
from django.core.paginator import Paginator
from django.contrib.auth.mixins import LoginRequiredMixin
from pinax.messages.models import Thread
from django.contrib import messages
from django.http import PermissionDenied, HttpResponseRedirect


class MyInboxListView(LoginRequiredMixin, InboxView, ListView):
    ...
    ...


class SendMessageView(LoginRequired, MessageCreateView, TemplateView):
    """
    Since we are inheriting from the MessageCreateView, we don't need to worry about that logic,
    We can just implement ours. We can override everything, GET, POST by applying specific logic on each
    HTTP verb or in general on the dispatch level
    """
    def dispatch(self, request, *args, **kwargs):
        if request.user.is_blocked: # OR WHATEVER LOGIC YOU HAVE IN YOUR USER
            message.add_message(request, messages.ERROR, "You can't send messages")
            raise PermissionDenied()
        return super().dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # SEND MESSAGE LOGIC HERE
        messages.add_message(request, messages.SUCCESS, "Message Sent")
        return HttpResponseRedirect('your reverse url')

This was very generic and I hope it helps you understand how to override and/or apply your own logic and still use pinax-messages resources. Personally, if I was pinax, I would put this also for django rest framework and it would be easier to work async but that is just me. I built my own.

Also, bare in mind that you can also use CreateView instead of TemplateView from Django, the difference is that with CreateView UpdateView they demand a form_class to be declared (your own or you can just reuse the pinax-messages one)

@reedjones
Copy link

@tiagoarasilva Thanks for the detailed reply! :)

So I guess by defining my own views with the same names, the url dispatcher will send to mine first?
That's what wasn't clear to me, and honestly I still don't see how will django know to use my view, instead of the pinax SendMessageView for example... Anyway thanks again, this already helped a lot I guess I just have to get my head around classed based views....

@tarsil
Copy link

tarsil commented Aug 6, 2020

@reedjones by default it will always go to your view first since the URL that you defined is pointing to your view and not the pinax (not declared in your app by importing them). Then, your view inheriting from the rest, the validations go from bottom-up (like any normal inheritance class, function, or paradigm), so you absolutely got it.

"That's what wasn't clear to me, and honestly I still don't see how will django know to use my view, instead of the pinax"

Simple, you have 2 options.

  1. You use pinax URLs (which I don't even declare in my app) and Django will always go there.
  2. You use your own URLs with your OWN views and those views are inheriting from the pinax-views as per example. This way, Django only points to your custom URLs (since you don't even declare the URLs of pinax, at least I didn't but I was using DRF) and your custom views inheriting from pinax will have the same result as pinax-views declared + your desired custom logic 😄 .

Makes sense?

@reedjones
Copy link

@tiagoarasilva
yes makes sense :) thank you..
I think I will go with your method of not using the pinax urls and just inheriting from the views...
We should make a pull request with your example in the documentation I feel like would help some others as well...
thanks again!

@KatherineMichel
Copy link
Member

@reedjones @tiagoarasilva Thank you for the assistance, Tiago. Even some of us who work with Pinax are overwhelmed at how to do things and explain at times. Now that a release is done, docs are a focus. Perhaps this info could be used, if you are ok with that.

@tarsil
Copy link

tarsil commented Aug 7, 2020

@KatherineMichel please by all means use what you need and want as long I could help. I would do it also for Django rest framework but that is a different example

@tarsil
Copy link

tarsil commented Aug 7, 2020

@KatherineMichel if you ever consider applying pinax-messages for DRF, please let me know since I have that already implemented as well 😄

@morenoh149 morenoh149 changed the title Can you customize the create view? [Request] Document customizing the class based views Aug 7, 2020
@morenoh149
Copy link
Author

if welcome, we should add the explanation at #55 (comment) to the documentation in the readme. OR better, add an example django app to the repo OR add a docs page (read the docs or github wiki).

@KatherineMichel what is your preference?

@tarsil
Copy link

tarsil commented Aug 7, 2020

@morenoh149 I've tried to contribute to pinax here #58. I can start doing it and also provide some DRF integrations if you all wish as well since DRF is widely used or just simply create a new pinax-messages-drf

@KatherineMichel
Copy link
Member

@tiagoarasilva @morenoh149 I would love to have a DRF example. We have had a lot of DRF interest recently. I think it would probably be best to put it in its own repo. We can link to it in the README and put a reference to it in the global docs. I will review your PR soon @tiagoarasilva. Thank you for submitting! I'm still thinking about what to do with the docs. If you ever have time @tiagoarasilva, would also love to pick your brain if you have any thoughts about how to approach Pinax onboarding for newcomers. I have a tutorial in process, but am sort of at an impasse about how to proceed.

@tarsil
Copy link

tarsil commented Aug 18, 2020

@KatherineMichel that is not a problem at all. I can just create a pinax-messages-drf and go from there, then you just need to fork it I would say?
Regarding the documents and onboarding, of course I can make time to improve and help :-). You just need to tell me what is your plan, what you have and what you would like me to do. You can even PM me if you wish.
Also, how would you like me to approach the DRF? I just build normally and pinax forks and changes or just references?

@tarsil
Copy link

tarsil commented Aug 18, 2020

@KatherineMichel I've created a quick but working and tested version using drf here https://github.com/tiagoarasilva/django-messages-drf if you want to have a look and if you are happy, we can have a chat to understand the needs. I didn't want to call pinax for obvious reasons but is heavily based on it of course :)

@morenoh149
Copy link
Author

imo, a drf example within this repo would be best. In my experience having examples outside of the project makes it more likely to go stale.

@tarsil
Copy link

tarsil commented Aug 18, 2020

So @morenoh149 what would you like to do? I just tried to help anyway. I've implemented a DEF version on my local a few months ago. Also, by quick I mean, took time but it wasn't a full scalable platform, only the drf version for pinax. I'm happy todo whatever for instance, I'm happy to donate it to pinax

@morenoh149
Copy link
Author

@KatherineMichel needs to weigh in. Whether the example should be in a separate repo or inside this repo as another example.

@KatherineMichel
Copy link
Member

I understand what you are saying, @morenoh149. We do have some projects that have gone stale, but a lot of the most popular ones are still maintained. The reason why I kind of want to keep it separate (for the time being, at least), is that one of the problems we've had in the past is that devs have done different things in different repos (there are dozens of Pinax apps), which I've been working to fix to make them easier to document. I don't think we have a project included in any other app repo. Once something like that is done in one repo, it makes the approach inconsistent across apps.

@tarsil
Copy link

tarsil commented Aug 20, 2020

So, the conclusion is to ignore and pinax will make a DRF example?

@KatherineMichel
Copy link
Member

@tiagoarasilva No, I think your version would be great to have. I think it would be better for it to be in its own repo right now, which is the original plan.

@tarsil
Copy link

tarsil commented Aug 20, 2020

Ok @KatherineMichel perfect then. I didn't add a lot of documentation but I mentioned that is based on pinax-messages so you can do as you wish. I hope I could help. I've added also some personal snippets that I've been doing for years regarding pagination, metaclasses and so on

@svb0866
Copy link

svb0866 commented Sep 22, 2020

@tiagoarasilva tiagoarasilva

Thank you for your explanation. It helped me a lot.
Can you give us some examples of model inheritance adding extra fields, modifying already existing fields in the Thread and Message models?

Thanks
-SVB

@tarsil
Copy link

tarsil commented Sep 22, 2020

@svb0866 I will try to provide some more examples if @KatherineMichel agrees as that takes me some more time and I'm currently on holiday but I will do my best. Is actually quite simple and I will try to do it as soon as possible but summarising, is pretty much overriding current definitions from the parents with the same names on the children classes where the children classes with the same attribute names can have different properties and definitions and when running the migrations, it will use yours instead of the current parent ones. Also, changing existing model fields on the current pinax models it would be possible if you change the source code or having your own version but that would make it not maintained by pinax but if you override in your inherited models the attributes you would have the same effect.

I'm not sure if I'm being clear.

@svb0866
Copy link

svb0866 commented Sep 22, 2020

@svb0866 I will try to provide some more examples if @KatherineMichel agrees as that takes me some more time and I'm currently on holiday but I will do my best. Is actually quite simple and I will try to do it as soon as possible but summarising, is pretty much overriding current definitions from the parents with the same names on the children classes where the children classes with the same attribute names can have different properties and definitions and when running the migrations, it will use yours instead of the current parent ones.

Thanks, @tiagoarasilva ! Enjoy your holiday.

@tarsil
Copy link

tarsil commented Sep 22, 2020

@svb0866 but thank you for your kind words, I just try to help as much as I can :-).

About what I said, even without examples at the moment, I hope it could give some clarity

@devinhadley
Copy link

devinhadley commented Sep 30, 2020

Hello, I am getting error: name 'dispatch' is not defined. Specifically I am trying to Inherit from MessageCreateView to run logic before and after a message is sent. The code is giving me dispatch is not defined when trying to return dispatch(request, *args, **kwargs).

class SendMessageView(LoginRequiredMixin, MessageCreateView, TemplateView):
    def dispatch(self, request, *args, **kwargs):
        going_to = self.kwargs['user_id']
        print(going_to)
        other_user = User.objects.get(id=going_to)
        if request.user.profile not in other_user.profile.friends.all():  # OR WHATEVER LOGIC YOU HAVE IN YOUR USER
            return http.HttpResponseForbidden("Cannot message a non-friend.")
        return dispatch(request, *args, **kwargs) #ERROR HAPPENS HERE

    def post(self, request, *args, **kwargs):
        # SEND MESSAGE LOGIC HERE
        return HttpResponseRedirect('#')

@tarsil
Copy link

tarsil commented Sep 30, 2020

@devinhadley I could spot your return. You should do return super().dispatch (request, *args, **kwargs) since you are inheriting from the views. Found in my example I missed that. Apologies.

@devinhadley
Copy link

devinhadley commented Oct 1, 2020

Thank you so much for your response! I actually did try this earlier and it did not work. But to be safe I copied your code and pasted it in and still got the error: 'SendMessageView' object has no attribute 'object'.

Edit: I fixed this placing this within the dispatch method:
self.object = self.get_object()

Although now I am getting an error: SendMessageView is missing a QuerySet

This is peculiar as I wish to display a form with no intention of Querying for any models.

@tarsil
Copy link

tarsil commented Oct 1, 2020

Hi @devinhadley that is actually odd since I did the same and it works like a charm so it would be great if you could provide some additional information like, python version, version of Django and more code snippets.

That was merely informative but a queryset is only required if you are inheriting from a view that implements a ListView. A CreateView shouldn't give you that error at all but an UpdateView would for instance.

@devinhadley
Copy link

devinhadley commented Oct 1, 2020

Yeah that what I am saying, it is very odd.

Python Version: 3.8.5
Django Version: 3.1.1

Class View (Same problem happens when I inherit from CreateView rather than TemplateView):

class SendMessageView(LoginRequiredMixin, MessageCreateView, TemplateView):

    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        going_to = self.kwargs['user_id']
        print(going_to)
        other_user = User.objects.get(id=going_to)
        if request.user.profile not in other_user.profile.friends.all(): 
            return http.HttpResponseForbidden("Cannot message a non-friend.")
        return super().dispatch(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # SEND MESSAGE LOGIC HERE
        return HttpResponseRedirect('#')

URL Routing:

path('friends/messages/send/<int:user_id>',
         SendMessageView.as_view(), name='send-message')

Edit: Issue may reside with self.object = self.get_object(). Although without this I get the error as described above.

@tarsil
Copy link

tarsil commented Oct 1, 2020

Oh, @devinhadley mine is this

class​ ​SendMessageView​(​LoginRequired​, ​MessageCreateView​, ​TemplateView​):
    ​"""​
​    Since we are inheriting from the MessageCreateView, we don't need to worry about that logic,​
​    We can just implement ours. We can override everything, GET, POST by applying specific logic on each​
​    HTTP verb or in general on the dispatch level​
​    """​
    ​def​ ​dispatch​(​self​, ​request​, ​*args​, ​**kwargs​):
        ​if​ ​request​.​user​.​is_blocked​: ​# OR WHATEVER LOGIC YOU HAVE IN YOUR USER​
            ​message​.​add_message​(​request​, ​messages​.​ERROR​, ​"You can't send messages"​)
            ​raise​ ​PermissionDenied​()
        ​return​ ​super​().​dispatch​(​request​, ​*args​, ​**kwargs​)

I don't declare any self.object like you are doing but in this case I think I know what are you trying to do.

If you want a self.object like that you still need to declare your get_object() inside your class. The Django documentation about the CreateView explains that really well but I can see at least in my example that self.object is not declared by any function or overwritten anywhere and I reckon that is where your code is failing since there is no self.object or no declaration of a get_object() anywhere declared in your view

@devinhadley
Copy link

Lol, this is very odd, I deleted the self.object and now it wants to work just perfectly. Thank you so much for your time and patience!

@tarsil
Copy link

tarsil commented Oct 1, 2020

@devinhadley I'm glad I could help you :-)

@devinhadley
Copy link

Oh, golly the post method appears to be skipped, a print statement is not printed to console after submitting the form. Maybe because when you return super().dispatch() it defaults back to the original class view?

@tarsil
Copy link

tarsil commented Jan 7, 2021

Btw, @morenoh149 @devinhadley @KatherineMichel @svb0866 @reedjones

https://github.com/tarsil/django-messages-drf is already in the version 1.0.3 and is what we talked about before but I made a lot of changes from the original design and added a lot of my own for DRF :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants