Allow only editing the current selected Foreign Key in Django Admin - python

So currently I have something like this:
Model:
class ConfirmEmail(models.Model):
report = models.ForeignKey(Report)
owner = models.CharField(max_length = 100)
emails = models.ManyToManyField(Sdm)
Admin:
#admin.register(ConfirmEmail)
class ConfirmEmailAdmin(admin.ModelAdmin):
change_form_template = 'admin/phone/index.html'
readonly_fields = ('owner',)
filter_horizontal = ('emails',)
list_display = ('owner','report')
I create these objects in code - meaning I set the report object. But what I would like in the Django admin is if I could allow a user to edit that report object but only the one set. They would be allowed to change it (so hopefully the drop down menu would no longer be there) so the nice pencil icon would still be there, but things like that "+" icon would be gone.
And this is not to say the user can't edit all reports, it's just that in the ConfirmEmail Admin they can only view that specific report attached to it.
I've been smacking away at this and can't seem to get it work.
I would also be inclined to just have the current Report Form embedded into the ConfirmEmail form - but don't know how I would go about doing that.

You should first introduce a model admin for your Report model, then override the has_add_permission function of your ReportAdmin.
#admin.register(Report)
class ReportAdmin(admin.ModelAdmin):
# whatever you want here
def has_add_permission(self, request):
return False
You can also remove/disable the + button using javascript in the page, but be aware that the user can cause damage if he knows the add url, or disables javascript.

Related

Wagtail filter page-childs-elements on the basis of logged-in user's permissions

I am working on a small site using Wagtail. This site is all about a "mainpage" and several "subpages". So far it is pretty simple! But, depending on what group the user (not admin) is in, the right subpages should show up!
See the following setup (minimized), to get an idea of what I am talking about.
If I set permissions on ToolKitPart (like requiring explicit user-login and group-membership), then the following is happening:
when going to the page using the fully qualified path, the user is requested to login and, in the case of insufficient rights, the user will not see the content!
when going to the ToolkitIndex-Page, all children are displayed, including the ones the user never should see, without the need to be logged in or being a member of a certain group.
class ToolkitIndex(Page):
def get_context(self, request):
# Update context to include only published posts, ordered by reverse-chron
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
context['pages'] = blogpages
return context
class ToolkitPart(Page):
body = StreamField([
('staff', MpStaff()),
('news', MpNews()),
('stuff', MpStuff()),
('teditor', blocks.RichTextBlock()),
('reditor', blocks.RawHTMLBlock()),
], blank=True)
content_panels = Page.content_panels + [
StreamFieldPanel('body'),
]
class MpNews(blocks.StructBlock):
head = blocks.TextBlock(required=True, help_text='Schlagzeile')
lead = blocks.TextBlock(required=False, help_text='Einleitung')
body = blocks.RichTextBlock(required=True, help_text='Inhalt')
image = ImageChooserBlock(required=False)
type = blocks.ChoiceBlock(
choices=[('default', 'Standard'),
('highlight', 'Hervorgehoben'),
], required=True)
class Meta:
template = 'home/mparts/toolkit_news.html'
icon = 'code'
Any idea how to solve this?
Assuming you've set these permissions up using Wagtail's private pages feature, these are stored in the PageViewRestriction model. Unfortunately Wagtail doesn't currently provide a way to apply these permission checks against anything other than the current page request, so you'd have to recreate this logic yourself to filter a queryset to the user's view permissions. This would be something like (untested):
from django.db.models import Q
class ToolkitIndex(Page):
def get_context(self, request):
context = super().get_context(request)
blogpages = self.get_children().live().order_by('-first_published_at')
if not request.user.is_authenticated:
blogpages = blogpages.public() # only pages with no view restrictions at all
else:
blogpages = blogpages.filter(
# pages with no view restrictions
Q(view_restrictions__isnull=True)
# pages restricted to any logged-in user
| Q(view_restrictions__restriction_type='login')
# pages restricted by group
| Q(view_restrictions__restriction_type='groups', view_restrictions__groups__in=request.user.groups.all())
)
Disclaimers:
This doesn't account for pages that are protected by a shared password
To be fully correct, we'd need to account for the fact that view restrictions propagate down the tree (and so a subpage may still be restricted even if it doesn't have a view restriction record directly attached to it); however, we're only looking at immediate children of the current page (which they evidently do have access to...) so that issue doesn't come up here.
PageViewRestriction is not a public Wagtail API and may change in future releases - in particular, see RFC 32 for a proposed change that may happen in the fairly near future.

How to populate choice form from db in Django?

I can't figure out how to populate choice form from db. I know about ModelChoiceForm but the problem seems to be slightly different.
I want user to choose which sector does he work in. For example: 'Finance','Electronics' etc. which I would do simple:
SECTOR_CHOICES = (('finance',_('Finance'),
'electronics',_('Electronics')...
))
But the problem is that I want admin of the web to be able to add new choices, remove choice etc.
What came to my mind is to create a simple Model called Sector:
class Sector(models.Model):
name = models.CharField(max_length=40)
and User would have new attribute sector = models.ModelChoice(Sector).
But I'm scared what would happend when admin changes or removes a sector which is already used, and more, what if he removes it and the sector attribute is required?
How to solve this problem?
I would just override the delete_model as custom action and there check if the selected sector object is in use.
def delete_model(modeladmin, request, queryset):
for obj in queryset:
if UserModel.objects.filter(sector=obj).exists():
# do not delete, just add some message warning the admin about it
else:
obj.delete()
class UserModelAdmin(admin.ModelAdmin):
actions = [delete_model]
# ...

Django Admin Inlines with many fields, possible to have inline add button create popup?

I've got 3 models:
class Top(models.Model):
toptitle = models.CharField(max_length=255, verbose_name='top title')
class Middle(models.Model):
top = models.ForeignKey(Top)
middletitle = models.CharField(max_length=255, verbose_name='middle title')
importantfield1 = models.TextField(verbose_name='important field 1')
importantfield2 = models.TextField(verbose_name='important field 2')
...
importantfield20 = models.TextField(verbose_name='important field 20')
Now when I'm viewing the Admin page for a Top, I want to see what Middles are related to it (and be able to add and edit Middles from this page or from a link on this page).
I can do that easily enough with inlines. The problem is it becomes unwieldy with so many (required) fields in Middle.
If I specify that the inline is only to show middletitle
class MiddleInline(admin.TabularInline):
model = MiddleInline
fields = ('middletitle',)
extra = 0
I've got two problems. The first is that there is no way for me to get to the page on which I can edit all of the fields for Middles that already exist (without having to go to the Admin menu, selecting Middle and having to find the right Middle there). The second problem is that if I try to add another Middle from this inline, it allows me to create a middle with just a middletitle, but leaving empty all of the required importantfields.
I've been able to deal with the first issue by adding a link to edit the object:
class MiddleInline(admin.TabularInline):
def changeform_link(self,instance):
if instance.id:
changeform_url = reverse(
'admin:myapp_middle_change', args=(instance.id,)
)
return 'Details'
return ''
changeform_link.allow_tags = True
changeform_link.short_description = '' # omit column header
model = MiddleInline
fields = ('middletitle','changeform_link')
extra = 0
but now I'm not sure how to deal with the second problem.
Ideally I'd like the 'Add Another Middle' section to open up a pop up for creating a new Middle (with the Top already set and having/requiring all of the importantfields), which when saved would refresh the inline.
Is there a way of doing this? Am I approaching this entirely wrong?
I'm pretty sure I had somewhat the same issue. My solution was to display two different fieldsets for two different situations.
The ModelAdmin class has a get_fieldsets(self,request, obj=None) functions which i override in my admin.py file.
so something like:
class MiddleInline(admin.TabularInline):
'''your stuff'''
def get_fieldsets(self, request, obj=None):
if obj is None:
fields = list(('''tuple of the fields you want to display if it's a new object'''))
else:
fields = list(('''tuple of the fields you want to display if it's not a new object'''))
return [(None, {'fields': fields})]
I am not quite entirely sure I got your question right, but I hope this can help!

How to modify field rendering behaviour based on state of other fields of model in django

Let's assume that I have following models:
class ScoutBook(models.Model):
troop = models.ForeignKey('Dictionary', limit_choices_to={'type' : 'Troop'}, related_name='+', blank=True, null=True)
class Dictionary(models.Model):
name = models.CharField(max_length=CHAR_FIELD_MAX_LEN, verbose_name="Nazwa")
active = models.BooleanField(verbose_name="Aktywny")
type = models.CharField(max_length=CHAR_FIELD_MAX_LEN, choices=DICTIONARY_CHOICES)
and I want to implement following logic:
when creating ScoutBook allow users to select only active troops, and when editing allow to select active troops or allow user to leave value unchanged (even if the troop is inactive). If I use limit_choices_to = {..., 'active' = True} troop that is inactive is absent from combo box in django admin.
So to be clear: let's assume that there are four troops in this system: Troop1, Troop2 and InactiveTroop, InactiveTroop2. On model creation I would like user to be able to choose Troop1 and Troop2. If model has troop field set to InactiveTroop2, I would like user to be able to choose between InactiveTroop2, Troop1 and Troop2.
I was looking at the django forms api and I didn't found obvious way do this. Moreover, in the application I'm developing there will be many such fields and many such models --- so solution must be pain free. I would rather not create new Form class for every model. I will be using mostly django admin to enable editing the database, and some read only views that will just list entries.
Ideally I would like to encapsulate this functionality in some custom field --- but fields have access to model instance on validate and save phase --- so I dont have access to it when I produce formfield.
This sounds like something you want to do in a form, not in the object itself. Create a ModelForm and override the ModelChoiceField like this:
from django import forms
class ScoutBookForm(forms.ModelForm):
troop = forms.ModelChoiceField(queryset=Troop.objects.filter(active=True))
class Meta:
model = ScoutBook
You can also override the clean method of ScoutBook to ensure it cannot ever be saved with an inactive Troop, though that may have some unintended consequences (e.g., you wouldn't be able to update a ScoutBook in the admin if the troop had gone inactive at some point in the past).
Well I had to hook into ModelForm creation. Attached Form inspects it's fields and if specific conditions are met it replaces model field queryset.
class DictionayModelForm(ModelForm):
def __init__(self, *largs, **kwargs):
super(DictionayModelForm, self).__init__(*largs, **kwargs)
if self.instance and self.instance.pk is not None:
for f in self.instance._meta.fields:
if isinstance(f, models.ForeignKey) and issubclass(f.rel.to, Dictionary):
model_field = self.fields[f.name]
value = getattr(self.instance, f.name, None)
if value and value not in model_field.choices:
model_field.queryset = Dictionary.objects.filter(Q(**f.rel.limit_choices_to) | Q(id = value.id))

Inline-like solution for Django Admin where Admin contains ForeignKey to other model

I have several Customers who book Appointments. Each Appointment has exactly one customer, though a customer can be booked for multiple appointments occurring at different times.
class Customer(model.Model):
def __unicode__(self):
return u'%s' % (self.name,)
name = models.CharField(max_length=30)
# and about ten other fields I'd like to see from the admin view.
class Appointment(models.Model):
datetime = models.DateTimeField()
customer = models.ForeignKey("Customer")
class Meta:
ordering = ('datetime',)
Now when an admin goes to browse through the schedule by looking at the Appointments (ordered by time) in the admin, sometimes they want to see information about the customer who has a certain appointment. Right now, they'd have to remember the customer's name, navigate from the Appointment to the Customer admin page, find the remembered Customer, and only then could browse their information.
Ideally something like an admin inline would be great. However, I can only seem to make a CustomerInline on the Appointment admin page if Customer had a ForeignKey("Appointment"). (Django specifically gives me an error saying Customer has no ForeignKey to Appointment). Does anyone know of a similar functionality, but when Appointment has a ForeignKey('Customer')?
Note: I simplified the models; the actual Customer field currently has about ~10 fields besides the name (some free text), so it would be impractical to put all the information in the __unicode__.
There is no easy way to do this with django. The inlines are designed to follow relationships backwards.
Potentially the best substitute would be to provide a link to the user object. In the list view this is pretty trivial:
Add a method to your appointment model like:
def customer_admin_link(self):
return 'Customer' % reverse('admin:app_label_customer_change %s') % self.id
customer_admin_link.allow_tags = True
customer_admin_link.short_description = 'Customer'
Then in your ModelAdmin add:
list_display = (..., 'customer_admin_link', ...)
Another solution to get exactly what you're looking for at the cost of being a bit more complex would be to define a custom admin template. If you do that you can basically do anything. Here is a guide I've used before to explain:
http://www.unessa.net/en/hoyci/2006/12/custom-admin-templates/
Basically copy the change form from the django source and add code to display the customer information.
Completing #John's answer from above - define what you would like to see on the your changelist:
return '%s' % (
reverse('admin:applabel_customer_change', (self.customer.id,)),
self.customer.name # add more stuff here
)
And to add this to the change form, see: Add custom html between two model fields in Django admin's change_form
In the ModelAdmin class for your Appointments, you should declare the following method:
class MySuperModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if obj:
# create your own model admin instance here, because you will have the Customer's
# id so you know which instance to fetch
# something like the following
inline_instance = MyModelAdminInline(self.model, self.admin_site)
self.inline_instances = [inline_instance]
return super(MySuperModelAdmin, self).get_form(request, obj, **kwargs)
For more information, browser the source for that function to give you an idea of what you will have access to.
https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L423
There is a library you can use it.
https://github.com/daniyalzade/django_reverse_admin
But if you want to use link to object in showing table you can like this code:
def customer_link(self, obj):
if obj.customer:
reverse_link = 'admin:%s_%s_change' % (
obj.customer._meta.app_label, obj.customer._meta.model_name)
link = reverse(reverse_link, args=[obj.customer.id])
return format_html('More detail' % link)
return format_html('<span >-</span>')
customer_link.allow_tags = True
customer_link.short_description = 'Customer Info'
And in list_display:
list_display = (...,customer_link,...)

Categories

Resources