Object-level permissions in Django - django

I have a ListView as follows, enabling me to loop over two models (Market and ScenarioMarket) in a template:
class MarketListView(LoginRequiredMixin, ListView):
context_object_name = 'market_list'
template_name = 'market_list.html'
queryset = Market.objects.all()
login_url = 'login'
def get_context_data(self, **kwargs):
context = super(MarketListView, self).get_context_data(**kwargs)
context['scenariomarkets'] = ScenarioMarket.objects.all()
context['markets'] = self.queryset
return context
The two market models are as follows:
class Market(models.Model):
title = models.CharField(max_length=50, default="")
current_price = models.DecimalField(max_digits=5, decimal_places=2, default=0.50)
description = models.TextField(default="")
shares_yes = models.IntegerField(default=0)
shares_no = models.IntegerField(default=0)
b = models.IntegerField(default=100)
cost_function = models.IntegerField(default=0)
open = models.BooleanField(default=True)
def __str__(self):
return self.title[:50]
def get_absolute_url(self):
return reverse('market_detail', args=[str(self.id)])
class ScenarioMarket(models.Model):
title = models.CharField(max_length=50, default="")
description = models.TextField(default="")
b = models.IntegerField(default=100)
cost_function = models.IntegerField(default=0)
most_likely = models.CharField(max_length=50, default="Not defined")
open = models.BooleanField(default=True)
def __str__(self):
return self.title[:50]
def get_absolute_url(self):
return reverse('scenario_market_detail', args=[str(self.id)])
And my user model is as follows:
class CustomUser(AbstractUser):
points = models.DecimalField(
max_digits=20,
decimal_places=2,
default=Decimal('1000.00'),
verbose_name='User points'
)
bets_placed = models.IntegerField(
default=0,
verbose_name='Bets placed'
)
net_gain = models.DecimalField(
max_digits=20,
decimal_places=2,
default=Decimal('0.00'),
verbose_name='Net gain'
)
class Meta:
ordering = ['-net_gain']
What I want happen is that different users see different sets of markets. For example, I want users from company X to only see markets pertaining to X, and same for company Y, Z, and so forth.
Four possibilities so far, and their problems:
I could hardcode this: If each user has a company feature (in addition to username, etc.), I could add a company feature to each market as well, and then use if tags in the template to ensure that the right users see the right markets. Problem: Ideally I'd want to do this through the Admin app: whenever a new market is created there, it would be specified what company can see it.
I could try to use Django's default permissions, which of course would be integrated with Admin. Problem: Setting a view permission (e.g., here) would concern the entire model, not particular instances of it.
From googling around, it seems that something like django-guardian might be what I ultimately have to go with. Problem: As I'm using a CustomUser model, it seems I might run into problems there (see here).
I came across this here on SO, which would enable me to do this without relying on django-guardian. Problem: I'm not clear on how to integrate that into the Admin app, in the manner that django-guardian seems able to.
If anyone has any advice, that would be greatly appreciated!

You can add some relationships between the models:
class Company(models.Model):
market = models.ForeignKey('Market', on_delete=models.CASCADE)
...
class CustomUser(AbstractUser):
company = models.ForeignKey('Company', on_delete=models.CASCADE)
...
then in your view you can simply filter the queryset as appropriate:
class MarketListView(LoginRequiredMixin, ListView):
context_object_name = 'market_list'
template_name = 'market_list.html'
login_url = 'login'
def get_queryset(self):
return Market.objects.filter(company__user=self.request.user)
Note, you don't need the context['markets'] = self.queryset line in your get_context_data; the queryset is already available as market_list, since that's what you set the context_object_name to.

Related

How to pass pk of detail view into fk of form

Good afternoon, I am fairly new to Django and I am not sure how to go about this.I have a Django 2.2 project with these models:
class Equipment(models.Model):
name = models.CharField(
max_length=15,
unique=True,
verbose_name='asset name')
asset_cat = models.ForeignKey('category',on_delete=models.PROTECT,verbose_name='asset category')
asset_loc = models.ForeignKey('location',on_delete=models.PROTECT,verbose_name='asset location')
state = models.ForeignKey('status',on_delete=models.PROTECT,verbose_name='status')
brand = models.CharField(
max_length=15,
unique=False,
blank=True)
model = models.CharField(
max_length=12,
unique=False,
blank=True,
verbose_name='model number')
def __str__(self):
return "{}".format(self.name)
def get_absolute_url(self):
return reverse('equipment-detail', args=[str(self.id)])
class Meta:
ordering = ['asset_cat', 'name']
verbose_name_plural = 'pieces of equipment'
class Action(models.Model):
name = models.ForeignKey('equipment',on_delete=models.PROTECT,verbose_name='asset name',blank=False)
dt = models.DateTimeField(
auto_now_add=True,
verbose_name='date and time of incident')
incident = models.TextField(
blank=True,
null=True)
CHANGE = 'CHANGE'
SERVICE = 'SERVICE'
ACTION_CHOICES = (
(CHANGE, 'CHANGE'),
(SERVICE, 'SERVICE')
)
act = models.TextField(
blank=True,
choices=ACTION_CHOICES,
null=True,
verbose_name='action taken')
act_detail = models.TextField(
verbose_name='action detail',
blank=False)
result = models.TextField(
blank=True,
null=True)
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('service-detail', args=[str(self.id)])
class Meta:
ordering = ['-dt']
verbose_name_plural = 'service calls'
I have an Equipment Detail View like this:
class EquipmentDetailView(generic.DetailView):
model = Equipment
template_name = 'equipment_detail.html'
def get_context_data(self, **kwargs):
context = super(EquipmentDetailView, self).get_context_data(**kwargs)
return context
The detail view has two buttons: edit and service. If I click edit I have a model form that allows me to edit that instance of the Equipment model successfully.
However, when I click the service button, my form comes up to create an instance of the Action model, but when I submit it tells me that the null value in name_id violates the not null constraint.
It looks like my question is, how can I pass equipment.id from the Equipment Detail view to action.name of the action create form and keep the service button concept?
Action Form:
class ServiceForm(forms.ModelForm):
class Meta:
model = Action
fields = ['incident', 'act_detail', 'result']
Action (actually service) view:
class EquipmentServiceView(generic.CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()
Assuming you don't want to go with simpliest solution to include name in form fields and have urls setup as:
/equipment/<id> - equipment detail view
/service - service (or action) create view
There are several ways of passing equipment id:
1) From url
We are going to change url to accept equipment_id. That means instead of /service you will have url /equipment/<equipment_id>/service.
Probably best solution - you will use URL according to REST architecture and will have very clear structure. Client can access page from anywhere (like just copy paste link from mail) and it will work.
urls.py:
urlpatterns = [
path('equipment/<int:pk>', EquipmentDetailView.as_view(), name='equipment-detail'),
path('equipment/<int:equipment_pk>/service', EquipmentServiceView.as_view(), name='service-create')
]
Your service button should look like this: service
and finally your view:
class EquipmentServiceView(CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()
def form_valid(self, form):
equipment_pk = self.kwargs['equipment_pk']
equipment = get_object_or_404(Equipment, pk=equipment_pk)
self.object = form.save(commit=False)
self.object.name = equipment
self.object.save()
return super().form_valid(form)
2) Session data
In case you want to preserve service url without adding equipment_id, you can store equipment id either in session data(on your server) or in cookies(on client). That's not exactly good - client have to go to EquipmentDetailView prior to creating Service, but this will keep your urls intact.
views.py:
class EquipmentDetailView(DetailView):
model = Equipment
template_name = 'equipment_detail.html'
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
request.session['last_equipment_pk'] = self.object.pk
return response
class EquipmentServiceView(CreateView):
template_name = 'equipment_service.html'
form_class = ServiceForm
queryset = Action.objects.all()
def form_valid(self, form):
equipment_pk = self.request.session.get('last_equipment_pk')
equipment = get_object_or_404(Equipment, pk=equipment_pk)
self.object = form.save(commit=False)
self.object.name = equipment
self.object.save()
return super().form_valid(form)
P.S.: name is bad field name for ForeignField - should be something like equipment or so. Those labels usually associate with CharField and expected to be strings.

How to apply autocomplete in manytomany field in django

I would like to apply the autocomplete option for my Preorder.preorder_has_products.through model in order to be able to load products with autocomplete(i tried unsuccessfully).Moreover, I have an inline implementation in order to be able to select for one preorder more than one product. The obstacle is that I have a manytomany field(preorder_has_products) as you can see below and I do not know how to implement the autocomplete.
models.py
class Preorder(models.Model):
client = models.ForeignKey(Client,verbose_name=u'Πελάτης')
preorder_date = models.DateField("Ημ/νία Προπαραγγελίας",null=True, blank=True, default=datetime.date.today)
notes = models.CharField(max_length=100, null=True, blank=True, verbose_name="Σημειώσεις")
preorder_has_products=models.ManyToManyField(Product,blank=True)
def get_absolute_url(self):
return reverse('preorder_edit', kwargs={'pk': self.pk})
class Product(models.Model):
name = models.CharField("Όνομα",max_length=200)
price = models.DecimalField("Τιμή", max_digits=7, decimal_places=2, default=0)
barcode = models.CharField(max_length=16, blank=True, default="")
eopyy = models.CharField("Κωδικός ΕΟΠΥΥ",max_length=10, blank=True, default="")
fpa = models.ForeignKey(FPA, null=True, blank=True, verbose_name=u'Κλίμακα ΦΠΑ')
forms.py
class PreorderHasProductsForm(ModelForm):
product = ModelChoiceField(required=True,queryset=Product.objects.all(),widget=autocomplete.ModelSelect2(url='name-autocomplete'))
class Meta:
model=Preorder.preorder_has_products.through
exclude=('client',)
def __init__(self, *args, **kwargs):
super(PreorderHasProductsForm, self).__init__(*args, **kwargs)
self.fields['product'].label = "Ονομα Προϊόντος"
PreorderProductFormSet = inlineformset_factory(Preorder,Preorder.preorder_has_products.through,form=PreorderHasProductsForm,extra=1)
my views.py for autocomplete
class NameAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
if not self.request.user.is_authenticated():
return Product.objects.none()
qs = Product.objects.all()
if self.q:
qs = qs.filter(product__istartswith=self.q)
return qs
my template is written based on this tutorial : https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html
and finally my url for autocomplete:
url(r'^name-autocomplete/$',views.NameAutocomplete.as_view(),name='name-autocomplete'),
My result based on the above snippets is depicted in the attached image.
what could be wrong? I guess one possible issue could be the reference to the manytomanyfield table. Any idea?

Django inline model formset with 2 models

First of all, please forgive for my newbie questions. I did copy most of the code, and try to understand from Django documents.
Code as below:
models.py
class Order(models.Model):
ORDER_CHOICES = (
('import', 'IMPORT'),
('export', 'EXPORT')
)
storage = models.ForeignKey(Storage, on_delete=models.PROTECT)
order_type = models.CharField(max_length=6, choices=ORDER_CHOICES)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now_add=True)
class Item(models.Model):
def random_barcode():
return str(random.randint(10000000, 99999999))
type = models.ForeignKey(Type, on_delete=models.CASCADE)
order = models.ForeignKey(Order, on_delete=models.CASCADE, null=True)
brand = models.ForeignKey(Brand, on_delete=models.CASCADE)
item_name = models.CharField(max_length=50, help_text='Name of goods, max 50 characters')
barcode = models.CharField(max_length=8, default=random_barcode, unique=True)
production_date = models.DateField()
expired_date = models.DateField()
def __str__(self):
return self.item_type
forms.py
class ItemForm(ModelForm):
class Meta:
model = Item
exclude = ['order',]
fields = ['type', 'brand', 'item_name', 'production_date', 'expired_date']
ItemFormSet = inlineformset_factory(Order, Item, form=ItemForm, extra=1)
views.py
class CreatePO(CreateView):
model = Order
context_object_name = 'orders'
template_name = 'storages/create_po.html'
fields = ['order_type', 'storage',]
*#dun't know how to write below code....*
1st question: how to use inline formset to write the CreatePO view?
2nd question: I need my create PO template as below picture, how to add a "Quantity" field?
This kind of template need Javascript, right? Any alternative solution? I have no knowledge with javascript.
First of all, move the def random_barcode(): before def __str__(self): it looks so ugly formated code.
Then let's have a look in your pic, if you haven't proper experience with Javascript you can use Admin Views from Django, it's much more simple and supported by Django 2.1. Read more if you would like to give permission to everyone in a admin-views page https://docs.djangoproject.com/el/2.1/releases/2.1/#model-view-permission
So quantity will be just added inside Item class
quantity = models.PositiveSmallIntegerField(default=1)
Also for your form, in my opinion, you need modelform_factory, so I suggest to read this one https://docs.djangoproject.com/en/2.1/topics/forms/modelforms/#modelform-factory-function

Django: Data uownership for users

I am trying to create a simple app. It has the following model:
Model: Product
class Product(models.Model):
product_owner = models.ForeignKey(User, verbose_name='User')
product_title = models.CharField(max_length=100, null=False,
verbose_name='Product title')
product_description = models.TextField(max_length=250, verbose_name='Product description')
product_qty = models.IntegerField(verbose_name='Quantity')
product_mrp = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='Maximum retail price')
product_sku = models.CharField(max_length=100, null=False, unique=True, verbose_name='SKU',help_text='Enter Product Stock Keeping Unit')
product_barcode = models.CharField(max_length=100, null=False, verbose_name='Barcode')
I am using only the built in Admin App provided by django framework. And I was able to Make the Product data availaible for only the respective user by adding the following in the Admin classes.
class ProductAdmin(ImportExportModelAdmin):
exclude = ('product_owner',)
list_display = ['product_title','product_description', 'product_qty',
'product_mrp','product_sku','product_barcode']
search_fields = ['product_title', 'product_description', 'product_sku',
'product_barcode']
ordering = ['id']
list_display_links = ['product_title']
def get_queryset(self, request):
if request.user.is_superuser:
return Product.objects.all()
return Product.objects.filter(product_owner=request.user)
def save_model(self, request, obj, form, change):
if not change:
obj.product_owner = request.user
obj.save()
As I just started experimenting, I added 2 users, User1 and User2.
For User1 I added 10 products. Then I loggen in as User2 and User2 cannot see the products added by User1.
Now when I am tying to add products for User2, and if there is a conflict in product_sku field which is a unique field, I cannot add the product at all.
How to solve this. I need each user to add his own products, but product_sku unique to his set of products data, not for the whole database.
Instead of defining unique=True on the product_sku field, you should define unique_together for sku and user.
class Product(models.Model):
...
class Meta:
unique_together = (('product_sku', 'product_owner'),)
This will ensure that each user can only have one product with a particular sku, but multiple users can have the same sku.

Try to join a OneToOne relationship in Django

I need some help doing a join using Django, which seems like it should be easy. I have looked at the documentation but it seems like it won't join for some reason.
I am trying to get in my view, the model.Photo and model.PhotoExtended with both joined and then displayed in the view. Currently I am just trying to get the model.Photo displayed but with a join which finds the request.user and filters it based on that.
They are in different apps.
models.py for model.Photo
class Photo(ImageModel):
title = models.CharField(_('title'),
max_length=60,
unique=True)
slug = models.SlugField(_('slug'),
unique=True,
help_text=_('A "slug" is a unique URL-friendly title for an object.'))
models.py for model.PhotoExtended
class PhotoExtended(models.Model):
Photo = models.OneToOneField(Photo, related_name='extended', help_text='Photo required', null=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, help_text='User that uploaded the photo')
views.py
class PhotoExtendedUserView(ListView):
template_name = 'photo_user_list.html'
def get_queryset(self):
user = get_object_or_404(User, username=self.request.user)
return Photo.objects.filter(photoextended__user=user)
You set the related_name on Photo (which shouldn't be capitalized by the way) to extended so you need to filter like so:
class PhotoExtendedUserView(ListView):
template_name = 'photo_user_list.html'
def get_queryset(self):
user = get_object_or_404(User, username=self.request.user)
# 'extended' vs. 'photoextended'
return Photo.objects.filter(extended__user=user)