I have 3 types of users in my models.py
class Customer(models.model)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name=‘Customer’)
class ClientTypeA(models.model)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name=‘ClientA’)
class ClientTypeB(models.model)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name=‘ClientB’)
I was using permissions in my base template to render the correlating sidebar, but now I am also incorporating a specific dashboard for each user along with the sidebar, so I find it would be simpler to create home views for each of the user types.
Once I log a user in it redirects them to my home view - so I came up with this in my views.py
def home(request):
if request.user.is_customer:
return redirect(customer_home)
elif request.user.is_clientA:
return redirect(clientA_home)
elif request.user.is_clientB:
return redirect(clientB_home)
else:
return render(request, 'home.html')
The redirects called will simply take them to there corresponding home pages.
I know my is_customer, is_clientA, is_clientB do not automatically come with django, how and where do I add these custom permissions? to my models.py? What would I set them equal to in order to simply check is the type of user active? Do I even need custom permissions or is there a simpler way to call the type of user?
Am I using to many if statements? (I'm trying to keep it simple and fast)
Since all the models have one-to-one fields to the User model, you can use has_attr to check whether the user has a row for that model. You don't have to create permissions.
if hasattr(request.user, 'customer'):
return redirect('customer_home')
If you only have three customer types, then the if/elif/else is probably ok. As the number increases, you can change it to something like the following.
customer_types = [
('customer', 'customer_home'),
('clienttypea', 'ClientA_home'),
...
]
for field_name, redirect_url in customer_types:
if hasattr(request.user, field_name):
return redirect(redirect_url)
# fallback
return render(request, 'home.html')
Related
I have a django page that displays a list of links. Each link points to the detail page of the respective object. The link contains the pk/id of that object (something like ../5/detailObject/). The list is generated on the backend and has some filtering baked into it, e.g. only generate a link if that object has state x, etc.
Clicking on the links works, but users can still manipulate the url and pass a valid link with an incorrect state (a wrong pk/id is being handled with the get or 404 shortcut).
What is the best practice for handling this kind of scenario with django? Should that kind of filtering be placed in the object's model class instead of using function-based views as I do now?
Function based view:
If you want to restrict a set of objects to a particular user (for instance a user's orders), then you would need to set up the Order model to foreign key to the User model and then look up the order by both id and user:
views.py:
def get_order(request, id=0)
if request.method == 'GET':
try:
order = Order.objects.get(user=request.user, pk=id)
except Order.DoesNotExist:
return redirect(...)
And set up a url to handle:
url(r'^order/(?P<id>\d+)/$', views.get_order, name='get_order_by_id'),
As far as adding a slug field on the model after the fact, set up a second url:
url(r'^order/(?P<slug>[\w-]+)/$', views.get_order, name='get_order_by_slug')
And change the above view logic to first do a lookup by pk if pk is greater than 0 and then redirect back to the function using the slug from the looked up order (this assumes all looked-up records have slugs):
def get_order(request, slug='', id=0)
if request.method == 'GET':
try:
if id > 0:
order = Order.objects.get(user=request.user, pk=id)
return redirect(reverse('get_order_by_slug'), permanent=True, slug=order.slug)
order = Order.objects.get(user=request.user, slug=slug)
except Order.DoesNotExist:
return redirect(...)
You should also put unique=True on the slug field and ensure that the user is authenticated by placing the #login_required decorator on your view.
To restrict orders by a particular status, you could:
Create a set of statuses for your Order model, and then you could:
Pass a value for a kwarg in the view when you filter, or
Create a custom manager on the Order model
There are several ways you could create your statuses:
as a set of choices on the Order model
use the SmartChoices library
as a database field
If you create choices on the Order model, it could be something like this:
class Order(models.model):
STATUSES = (
('PLCD', 'Placed'),
('INTR', 'In Transit'),
('DLVR', 'Delivered')
)
status = models.CharField(max_length=4, default='', choices=STATUSES)
An acquaintance who is a very seasoned Django professional told me about the SmartChoices library. I have not used it yet but would like to try it at some point. The database field option would be my least preferred way of doing this because that seems to me like moving programming variables into the database; however, it would work.
I am creating an expense submission system, which will be multi-user.
For the purpose of this question, there are two models: Claim and Journey. A user creates a claim and each claim can have multiple journeys. I have made a gist of the code snippet as it's quite long.
In this snippet, I have sucessfully:
Made ClaimListView.get_queryset filter by current user, so whoever's logged in can only see a list of their own claims.
Made ClaimCreateView.form_valid set the correct user when the form is submitted.
Made ClaimDetailView.get_queryset filter by current user. If someone tries the url for another user's claim detail, they get a 404 (perfect!)
Done the same as above for JourneyListView
Done the same as above for JourneyDetailView - again 404 if not authroised :D
However, when I access JourneyCreateView via the URL, the dropdown box for claim still shows claims for the other users.
How should I filter the user within the JourneyCreateView class, so that the claim field only shows claims assigned to the current user?
The closest to a solution I've got is this answer which suggests overriding the __init__ function in the JourneyForm which would leave me with this:
class JourneyForm(forms.ModelForm):
class Meta:
model = Journey
fields = ['date', 'distance','claim']
def __init__(self,alloweduser,*args,**kwargs):
super (JourneyForm,self ).__init__(self,*args,**kwargs) # populates the post
self.fields['claim'].queryset = Claim.objects.filter(tech_id=alloweduser)
However I'm not sure how to pass the alloweduser in from JourneyCreateView or, more to the point, obtain the current user in this class.
form_valid isn't any use in this case, as I'm trying to obtain the user prior to the form being submitted.
In views, the request the view is handling is stored in self.request, so you can obtain the user with self.request.user, and its id with self.request.user.id.
A Django view with the FormMixin [Django-doc] has a method that can be overwritten to pass parameters: get_form_kwargs() [Django-doc].
So we can implement this as:
from django.views.generic.edit import CreateView
class JourneyCreateView(CreateView):
model = Journey
form_class = JourneyForm
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs(*args, **kwargs)
kwargs['alloweduser'] = self.request.user.id
return kwargs
# ...
How can permissions be applied to individual fields of a Wagtail page?
Let's say we have a page like this one:
class HomePage(Page):
body = RichTextField(blank=True)
content_panels = Page.content_panels + [
FieldPanel('body', classname="full"),
]
Everyone should be allowed to edit the title - but only users with a certain permission should be able to alter the body.
I realize this is a really old question now, but just in case people come across it in the future, here's how my code shop does this in Wagtail 2.3. It may or may not work in later versions.
Add the following to the Page subclass you've written:
panels = [
...
MultiFieldPanel(
[
FieldPanel('display_locations', widget=forms.CheckboxSelectMultiple),
StreamFieldPanel('assets'),
],
heading='Admin-only Fields',
# NOTE: The 'admin-only' class is how EventPage.get_edit_handler() identifies this MultiFieldPanel.
classname='collapsible admin-only'
),
...
]
class Meta:
verbose_name = 'Event'
verbose_name_plural = 'Events'
ordering = ['start_date', 'title']
permissions = (
('can_access_admin_fields', 'Can access Event Admin fields'),
)
#classmethod
def get_edit_handler(cls):
"""
We override this method (which is added to the Page class in wagtail.admin.edit_handlers) in order to enforce
our custom field-level permissions.
"""
# Do the same thing that wagtail.admin.edit_handlers.get_edit_handler() would do...
bound_handler = cls.edit_handler.bind_to_model(cls)
# ... then enforce admin-only field permissions on the result.
current_request = get_current_request()
# This method gets called during certain manage.py commands, so we need to be able to gracefully fail if there
# is no current request. Thus, if there is no current request, the admin-only fields are removed.
if current_request is None or not current_request.user.has_perm('calendar_app.can_access_admin_fields'):
# We know for sure that bound_handler.children[0].children is the list of Panels in the Content tab.
# We must search through that list to find the admin-only MultiFieldPanel, and remove it.
# The [:] gets us a copy of the list, so altering the original doesn't change what we're looping over.
for child in bound_handler.children[0].children[:]:
if 'admin-only' in child.classname:
bound_handler.children[0].children.remove(child)
break
return bound_handler
This is, obviously, quite funky and fragile. But it's the only solution I could find.
Wagtail do not support field based permission control. But you can achieve this by enabling/disabling fields of the page's form. If you add a field to a page, you can pass whether that field should be enabled or disabled in the constructor. But how can you do that runtime?
Every HTML page with a form in wagtail is associated with a Django Form. You can change the default form to a your own one like this.
from django.db import models
from wagtail.admin.forms.pages import WagtailAdminPageForm
from wagtail.core.models import Page
from wagtail.admin.edit_handlers import FieldPanel
class HomeForm(WagtailAdminPageForm):
pass
class HomePage(Page):
body = models.CharField(max_length=500, default='', blank=True)
content_panels = Page.content_panels + [
FieldPanel('body'),
]
base_form_class = HomeForm # Tell wagtail to user our form
This way, every time you load the create view or edit view of HomePage, wagtail will create an instance from HomeForm class. Now what you have to do is, to check the user status and enable/disable the required field when creating an instance of the HomeForm.
For this example, I will enable the body field only when the user is a superuser.
class HomeForm(WagtailAdminPageForm):
# Override the constructor to do the things at object creation.
def __init__(self, data=None, files=None, parent_page=None, subscription=None, *args, **kwargs):
super().__init__(data, files, parent_page, subscription, *args, **kwargs)
user = kwargs['for_user'] # Get the user accessing the form
is_superuser = user.is_superuser
body = self.fields.get('body') # Get the body field
body.disabled = not is_superuser # Disable the body field if user is not a superuser
This way, every time a non superuser loads the create or edit page, the body field will be disabled.
But if you want to remove access only from edit page, you need to use self.initial variable. This variable is a dictionary with initial values to be used when showing the form.
You can check the value of a required field (like title) from self.initial and if the field's value is empty, that means the create page is loaded and if there is a value, that means the edit page is loaded.
I would like to create an API for my project.
The models:
class Offer(models.Model):
user = models.ForeignKey(User)
data = models.TextField()
class Bid(models.Model):
user = models.ForeignKey()
offer = models.ForeignKey()
created_at = models.DateTimeField(auto_now_add=True)
Here's the simplified example (as there are more checks)
Both Offer & Bid users can cancel the Bid.
In my standard (HTML) view:
def cancel_bid(request, pk):
do_some checks_if request.user_is_either_Bid_or+Offer)creator()
check_if_Bid_has_been_created_for_less_than_5_minutes()
#as user can cancel his bid within 5 minutes
Now the same must be applied to Django Rest Framework (permissions or serializers).
The problem is that I need to return error messages and codes displayed in both json error response (when using api) and in my HTML views.
I have created a cancel(self, user, other kwargs) in my Bid model where check for these are performed and my custom PermissionDenied(message, code) is returned. Then in DRF and my views I simply put:
bid = Bid.objects.get(pk=pk)
try:
bid.cancel(user):
except PermissionDenied as e:
messages.error(request, e.message)
# or return HttpResponseForbidden(e.message)
in django rest framework:
class OrderCancelView(APIView):
def post(self, request, pk):
try:
order.cancel(request.user)
except PermissionDenied as e:
raise serializers.PermissionDenied(detail=e.message)
There are more actions performed in my clean() method not pointed out here.. which
makes the method very complex.
For example:
If offer expires all bids except the first one (earliest) are cancelled.. so there is no user,
as it's being made by system. I have to omit user checks then.. and so on.
What are your thoughts on this ? What is the best Django way of doing this kind of "action" validation and keeping DRY rule?
Seems like you've already made a good start, you just need to structure your cancel method on the model so that it has all of the information it needs to execute the business logic. If your logic has to be the same between DRF and the normal view code then I would extract all the business logic into the model. You might have to give it lots of arguments so that it can make all of the correct decisions, but that's better than having to repeat the logic twice.
I have a django model which stores user and product data from a form input:
def ProductSelection(request, template_name='product_selection.html'):
...
if user.is_authenticated():
user = request.user
else:
# deal with anonymous user info
project = Project.objects.create(
user=user,
product=form.cleaned_data["product"],
quantity=form.cleaned_data["product_quantity"],
)
Of course this is fine for authenticated users, but I also want to be able to store anonymous user projects, and if possible, associate them with the user when they eventually register and authenticate.
My idea is to create anonymous user with name = some_variable (timestamp concatenated with a random hash?), then save that username in session data. If I ensure that that session variable, if exists, is used to record all projects activity of that user, I should be able to update the projects with the user's real credentials when they register.
Is this overly complicated and brittle? Do I risk saving thousands of rows of data unnecessarily? What would be the optimal approach for this common issue?
Any guidance on this would be much appreciated.
You can use Django's session framework to store anonymous user data.
You can then either add a field to your Project model to hold the session_key value for anonymous users,
project = Project.objects.create(
user=request.user, # can be anonymous user
session=request.session.session_key,
product=form.cleaned_data["product"],
quantity=form.cleaned_data["product_quantity"])
or simply store all the data a Project instance would have in the session
if user.is_authenticated():
project = Project.objects.create(
user=request.user,
product=form.cleaned_data["product"],
quantity=form.cleaned_data["product_quantity"])
else:
# deal with anonymous user info
request.session['project'] = {
"product": form.cleaned_data["product"],
"quantity": form.cleaned_Data["product_quantity"]}
You can retrieve the data from the session later, when creating a proper user.
Just to clarify, the below code is how implemented the solution in my case:
project = Project.objects.create(
session=request.session.session_key,
# save all other fields
...
)
if request.user.is_authenticated():
project.user = request.user
else:
# make a copy of the session key
# this is done because the session_key changes
# on login/ register
request.session['key_copy'] = request.session.session_key
project.save()
And in my models.py:
class Project(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
...
So a user field can be null, and in this case we use the session_key to keep a track of things.