I have some fields in page disabled as for example:(using jinja2 templating system)
<html>
<body>
<form action="" method=POST>
{{ form.name(disabled=True) }}
{{ form.title }}
-- submit button --
</form>
</body>
</html>
Field is disabled in the form as expected.
In my views.py: On doing validate_on_submit() on form submit, it fails with validation error on 'name' field which is disabled. I was hoping that validation ignores disabled field. Is it the right behaviour? If so, can you please let know how to handle such a case?
Updated:
class TeamForm(wtf.Form):
name = wtf.TextField("Team Name", validators=[validators.Required()])
title = wtf.TextField("Title", validators=[validators.Required()])
This is actually an interesting problem, and the way WTForms solves it is intentionally something that requires explicitness, because it has to do with security and not allowing users to fake input.
So the intent is, that "managers" cannot edit the name, while "admins" can.
At first glance this seems obvious, just disable the field in HTML, and write your view like this:
def edit_team():
form = TeamForm(request.POST, obj=team)
if request.POST and form.validate():
form.populate_obj(team) # <-- This is the dangerous part here
return redirect('/teams')
return render('edit_team.html')
As written, this is a major security risk, because the disabled property in HTML forms is client-side only. Anyone with an HTML inspector (ie FireBug, webkit document inspector, etc) can remove this property, or someone could simply make a request like so:
POST /edit_team/7 HTTP/1.0
Content-Type: application/x-urlencoded
team=EVILTEAMNAME&title=foo
The issue then is of course, how do we gate this properly on the server-side, corresponding to the appropriate way of doing this? The correct approach with WTForms is to not have the field in the first place. There's a few ways to do this, one is to use form composition and have e.g. ManagerTeamForm and AdminTeamForm (sometimes this is better) but other times it's easier to use del to remove specific fields.
So here's how you would write your view, and not have the validation issues:
def edit_team():
form = TeamForm(request.POST, obj=team)
if user.role == 'manager':
del form.name
if request.POST and form.validate():
form.populate_obj(team)
return redirect('/teams')
return render('edit_team.html')
And a quick modification to the template:
<html>
<body>
<form action="" method=POST>
{% if 'name' in form %}
{{ form.name() }}
{% else %}
{{ team.name|e }}
{% endif %}
{{ form.title }}
-- submit button --
</form>
</body>
</html>
Some pieces of reference for wtforms best-practices:
WTForms 'Solving Specific Problems'
Dangers of Using forms as a backing store (WTForms google group) Post 1 / Post 2
StackOverflow: WTForms 'readonly' attribute
You need to make the name field optional when defining the form.
name = wtf.TextField("Team Name", validators=[validators.Optional()])
Then in your views, pass a variable called "role" and set it to either manager or admin depending on the user.
<form action="" method=POST>
{% if role == 'manager' % }
{{ form.name(disabled=True) }}
{% else % }
{{ form.name() }}
{{ form.title }}
-- submit button --
</form>
I defined my own validator for this problem:
from wtforms.validators import Optional
class OptionalIfDisabled(Optional):
def __call__(self, form, field):
if field.render_kw is not None and field.render_kw.get('disabled', False):
field.flags.disabled = True
super(OptionalIfDisabled, self).__call__(form, field)
And then I defined a new base for my forms:
from wtforms.form import Form
class BaseForm(Form):
def populate_obj(self, obj):
for name, field in self._fields.items():
if not field.flags.disabled:
field.populate_obj(obj, name)
Now every form can extend the BaseForm and disable fields like this:
from wtforms.fields import StringField, SubmitField
class TeamForm(BaseForm):
team = StringField(label='Team Name',
validators=[OptionalIfDisabled(), InputRequired()]
submit = SubmitField(label='Submit')
def __init__(self, *args, **kwargs):
super(TeamForm, self).__init__(*args, **kwargs)
# disable the fields if you want to
if some_condition:
self.team.render_kw = {'disabled': True}
After validation of the TeamForm, you can use populate_obj to copy the enabled form data in any object. It will ignore the disabled fields.
Create a custom validator
from wtforms.validators import Optional
class DisabledValidator(Optional):
"""
do nothing
"""
pass
Let's create a custom rule basing on the form.rule
from flask_admin.form.rules import Field
class EasyCustomFieldRule(Field):
def __init__(self, field_name, render_field='lib.render_field', field_args={}):
super(self.__class__, self).__init__(field_name, render_field)
self.extra_field_args = field_args
def __call__(self, form, form_opts=None, field_args={}):
field = getattr(form, self.field_name)
if self.extra_field_args.get('disabled'):
field.validators.append(DisabledValidator())
field_args.update(self.extra_field_args)
return super(self.__class__, self).__call__(form, form_opts, field_args)
Override write some functions of wtforms.form
from wtforms.form import Form
from wtforms.compat import iteritems
class BaseForm(Form):
"""
重写部分方法,以适应disabled的Field
"""
def validate(self):
"""
Validates the form by calling `validate` on each field, passing any
extra `Form.validate_<fieldname>` validators to the field validator.
"""
extra = {}
for name in self._fields:
inline = getattr(self.__class__, 'validate_%s' % name, None)
if inline is not None:
extra[name] = [inline]
return self.validate_(extra)
def validate_(self, extra_validators=None):
self._errors = None
success = True
for name, field in iteritems(self._fields):
is_disabled = False
for v in field.validators:
if isinstance(v, DisabledValidator):
field.flags.disabled = True
is_disabled = True
break
if is_disabled:
continue
if extra_validators is not None and name in extra_validators:
extra = extra_validators[name]
else:
extra = tuple()
if not field.validate(self, extra):
success = False
return success
def populate_obj(self, obj):
for name, field in self._fields.items():
if not field.flags.disabled:
field.populate_obj(obj, name)
set the form_base_class in your ModelView, and set the form_edit_rules or the form_create_rules with EasyCustomFieldRule
from flask_admin.contrib.sqla import ModelView
class MyTestModelView(ModelView):
...
form_base_class = BaseForm
...
form_edit_rules = (
EasyCustomFieldRule('column0', field_args={'disabled': True}),
'column1', 'column2'
)
Just testing...
Related
I plan to put two forms in one page in my flask app, one to edit general user information and the other to reset password. The template looks like this
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block page_content %}
<div class="page-header">
<h1>Edit Profile</h1>
</div>
{{ wtf.quick_form(form_profile, form_type='horizontal') }}
<hr>
{{ wtf.quick_form(form_reset, form_type='horizontal') }}
<hr>
{% endblock %}
Each form has a submit button.
In the route function, I tried to separate the two form like this
form_profile = ProfileForm()
form_reset = ResetForm()
if form_profile.validate_on_submit() and form_profile.submit.data:
....
if form_reset.validate_on_submit() and form_reset.submit.data:
.....
But it didn't work. When I click on the button in the ResetForm, the ProfileForm validation logic is executed.
I suspect the problem is that wtf.quick_form() creates two identical submit buttons, but not sure.
What should I do in this case? Can bootstrap/wtf.html template deal with this situation?
Define this two SubmitField with different names, like this:
class Form1(Form):
name = StringField('name')
submit1 = SubmitField('submit')
class Form2(Form):
name = StringField('name')
submit2 = SubmitField('submit')
Then in view.py:
....
form1 = Form1()
form2 = Form2()
if form1.submit1.data and form1.validate_on_submit(): # notice the order
....
if form2.submit2.data and form2.validate_on_submit(): # notice the order
....
Now the problem was solved.
If you want to dive into it, then continue read.
Here is validate_on_submit():
def validate_on_submit(self):
"""
Checks if form has been submitted and if so runs validate. This is
a shortcut, equivalent to ``form.is_submitted() and form.validate()``
"""
return self.is_submitted() and self.validate()
And here is is_submitted():
def is_submitted(self):
"""
Checks if form has been submitted. The default case is if the HTTP
method is **PUT** or **POST**.
"""
return request and request.method in ("PUT", "POST")
When you call form.validate_on_submit(), it check if form has been submitted by the HTTP method no matter which submit button was clicked. So the little trick above is just add a filter (to check if submit has data, i.e., form1.submit1.data).
Besides, we change the order of if, so when we click one submit, it only call validate() to this form, preventing the validation error for both form.
The story isn't over yet. Here is .data:
#property
def data(self):
return dict((name, f.data) for name, f in iteritems(self._fields))
It return a dict with field name(key) and field data(value), however, our two form submit button has same name submit(key)!
When we click the first submit button(in form1), the call from form1.submit1.data return a dict like this:
temp = {'submit': True}
There is no doubt when we call if form1.submit.data:, it return True.
When we click the second submit button(in form2), the call to .data in if form1.submit.data: add a key-value in dict first, then the call from if form2.submit.data: add another key-value, in the end, the dict will like this:
temp = {'submit': False, 'submit': True}
Now we call if form1.submit.data:, it return True, even if the submit button we clicked was in form2.
That's why we need to define this two SubmitField with different names. By the way, thanks for reading(to here)!
Thanks for nos's notice, he add an issue about validate(), check the comments below!
I have a little question regarding Forms / Views which don't use a Model object. I seem to have it set up almost the way it should, but I can't seem to figure out how to pass data around to initialise the fields in my edit form.
What I have to do is get data from a REST server which was developed using Delphi. So this django thingie won't be using the normal django ORM model thing. Currently I have it working so my app displays a list of departmets which it got using a REST call to the server. Each department has it's ID as a hyperlink.
My next step / thing I would like to do is display a form in which the user can edit some values for the selected department. Logically everything seems to be hooked up together the way it should (as far as I can see). Sadly ... for whatever reason ... I can't seem to pass along information about the clicked ID or even the selected object in my list to the detail view.
Would anyone be able to help me out ? This is what I have so far :
The urls.py :
# DelphiClient/urls.py
from django.conf.urls import patterns
from django.conf.urls import url
from . import views
urlpatterns = patterns("",
url(
regex=r"^Departments$",
view=views.DelphiDepartmentsListView.as_view(),
name="Departments"
),
url(
regex=r'^Department/(?P<pk>\d+)/$',
view=views.DepartmentFormView.as_view(),
name='department_update'
),
)
The views.py :
# DelphiClient/views.py
...
from .client import DelphiClient
from .forms import DepartmentForm
class DelphiDepartmentsListView(TemplateView):
template_name = 'DelphiDepartmentList.html'
def get_context_data(self, **kwargs):
client = DelphiClient()
departments = client.get_department()
context = super(DelphiDepartmentsListView, self).get_context_data(**kwargs)
context['departments'] = departments
#client.update_department(1, 'Update From Django')
return context
class DepartmentFormView(FormView):
template_name = 'DepartmentUpdate.html'
form_class = DepartmentForm
success_url = '/DelphiClient/Departments'
def get_initial(self, **kwargs):
"""
Returns the initial data to use for forms on this view.
"""
initial = super(DepartmentFormView, self).get_initial(**kwargs)
# How can I get the ID passed along from the list view
# so I can get the correct object from my REST server and
# pass it along in the Initial ???
return initial
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
print "form.data {0}".format(form.data)
client = DelphiClient()
client.update_department(form.data["flddepartmentId"],form.data["flddepartmenet"])
return super(DepartmentFormView, self).form_valid(form)
The forms.py :
# DelphiClient/forms.py
from django import forms
from .client import DelphiClient
class DepartmentForm(forms.Form):
# How can I fill in the values for these fields using an object passed in
# thhrough Initial or the context?
flddepartmentId = forms.IntegerField(label="Department ID") #, value=1)
flddepartmenet = forms.CharField(label="New Description", max_length=100)
def update_department(self, *args, **kwargs):
#print "update_department"
#print self.data
#print self.data["flddepartmenet"]
client = DelphiClient()
client.update_department(self.data["flddepartmentId"],self.data["flddepartmenet"])
And the template for the form :
<h1>Update Department</h1>
<p>Update Department? {{ department.flddepartmentid }}</p>
<p>Something : {{ something }}</p>
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<p><label for="id_flddepartmentId">Department ID:</label> <input id="id_flddepartmentId" name="flddepartmentId" type="number" value="1"></p>
<p><label for="id_flddepartmenet">New Description:</label> <input id="id_flddepartmenet" maxlength="100" name="flddepartmenet" type="text"></p>
<input type="submit" value="OK">
</form>
As you can see ... I'm close ... but no cigar yet :-) Since I'm completely new to Python / Django and have been learning on the go, I have no idea what I'm doing wrong or where I should look.
If anyone would be able to help or point me in the right direction it would be really appreciated.
The positional and name-based arguments are stored in self.args and self.kwargs respectively (see the docs on name based filtering). Therefore you can access the pk with self.kwargs['pk'].
I'm not sure that you should include flddepartmentId as an editable field in the form. It means that users could go to /Department/1/, but then enter flddepartmentId=2 when they submit the form. It might be better to remove the field from the form, then use the value from the URL when calling update_department.
client.update_department(self.kwargs['pk'],self.data["flddepartmenet"])
If you are sure that you want to include flddepartmentId in your form, then your get_initial method should look as follows:
def get_initial(self, **kwargs):
"""
Returns the initial data to use for forms on this view.
"""
initial = super(DepartmentFormView, self).get_initial(**kwargs)
initial['flddepartmentId'] = self.kwargs['pk']
return initial
is there a way i could send a form's (css) class from python?
For example:
class Company(Form):
companyName = TextField('Company Name', [validators.Length(min=3, max = 60)])
This renders a simple text field, but i want that text field to have the css class of .companyName, is that possible directly from python?
I know that i can put a id="companyName" directly from python, but not class.
Help.
Update:
I tried class_="companyName" and it did not work, i got:
__init__() got an unexpected keyword argument '_class'
Alternatively you can add the class in your template like this for jinja2:
{{ form.name(size=20, class_='input-small') }}
WTForms does not allow you to set display options (such as class name) in the field initialization. However, there are several ways to get around this:
If all of your fields should include a class name as well as an ID then just pass in each field's short_name to it when you render it:
<dl>
{% for field in form %}
<dt>{{field.label}}</dt>
<dd>{{field(class_=field.short_name)}}</dd>
{% endfor %}
</dl>
Create a custom widget mixin that provides the class name:
from wtforms.fields import StringField
from wtforms.widgets import TextInput
class ClassedWidgetMixin(object):
"""Adds the field's name as a class
when subclassed with any WTForms Field type.
Has not been tested - may not work."""
def __init__(self, *args, **kwargs):
super(ClassedWidgetMixin, self).__init__(*args, **kwargs)
def __call__(self, field, **kwargs):
c = kwargs.pop('class', '') or kwargs.pop('class_', '')
kwargs['class'] = u'%s %s' % (field.short_name, c)
return super(ClassedWidgetMixin, self).__call__(field, **kwargs)
# An example
class ClassedTextInput(ClassedWidgetMixin, TextInput):
pass
class Company(Form):
company_name = StringField('Company Name', widget=ClassedTextInput)
Use render_kw if using WTForms >= 2.1 :
submit = SubmitField(u'Block Submit Buttom', render_kw={"class": "btn btn-primary btn-block"})
In your template, try it
{{ form.companyName( **{'class': 'companyName'} ) }}
To deal with the lack of nested inlines in django-admin, I've put special cases into two of the templates to create links between the admin change pages and inline admins of two models.
My question is: how do I create a link from the admin change page or inline admin of one model to the admin change page or inline admin of a related model cleanly, without nasty hacks in the template?
I would like a general solution that I can apply to the admin change page or inline admin of any model.
I have one model, post (not its real name) that is both an inline on the blog admin page, and also has its own admin page. The reason it can't just be inline is that it has models with foreign keys to it that only make sense when edited with it, and it only makes sense when edited with blog.
For the post admin page, I changed part of "fieldset.html" from:
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field }}
{% endif %}
to
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{% ifequal field.field.name "blog" %}
<p>{{ field.field.form.instance.blog_link|safe }}</p>
{% else %}
{{ field.field }}
{% endifequal %}
{% endif %}
to create a link to the blog admin page, where blog_link is a method on the model:
def blog_link(self):
return '%s' % (reverse("admin:myblog_blog_change",
args=(self.blog.id,)), escape(self.blog))
I couldn't find the id of the blog instance anywhere outside field.field.form.instance.
On the blog admin page, where post is inline, I modified part of "stacked.html" from:
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{{ inline_admin_form.original }}
{% else %}#{{ forloop.counter }}{% endif %}</span>
to
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{% ifequal inline_admin_formset.opts.verbose_name "post" %}
<a href="/admin/myblog/post/{{ inline_admin_form.pk_field.field.value }}/">
{{ inline_admin_form.original }}</a>
{% else %}{{ inline_admin_form.original }}{% endifequal %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
to create a link to the post admin page since here I was able to find the id stored in the foreign key field.
I'm sure there is a better, more general way to do add links to admin forms without repeating myself; what is it?
Use readonly_fields:
class MyInline(admin.TabularInline):
model = MyModel
readonly_fields = ['link']
def link(self, obj):
url = reverse(...)
return mark_safe("<a href='%s'>edit</a>" % url)
# the following is necessary if 'link' method is also used in list_display
link.allow_tags = True
New in Django 1.8 : show_change_link for inline admin.
Set show_change_link to True (False by default) in your inline model, so that inline objects have a link to their change form (where they can have their own inlines).
from django.contrib import admin
class PostInline(admin.StackedInline):
model = Post
show_change_link = True
...
class BlogAdmin(admin.ModelAdmin):
inlines = [PostInline]
...
class ImageInline(admin.StackedInline):
# Assume Image model has foreign key to Post
model = Image
show_change_link = True
...
class PostAdmin(admin.ModelAdmin):
inlines = [ImageInline]
...
admin.site.register(Blog, BlogAdmin)
admin.site.register(Post, PostAdmin)
This is my current solution, based on what was suggested by Pannu (in his edit) and Mikhail.
I have a couple of top-level admin change view I need to link to a top-level admin change view of a related object, and a couple of inline admin change views I need to link to the top-level admin change view of the same object. Because of that, I want to factor out the link method rather than repeating variations of it for every admin change view.
I use a class decorator to create the link callable, and add it to readonly_fields.
def add_link_field(target_model = None, field = '', link_text = unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
cls.link = link
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + ['link']
return cls
return add_link
You can also pass a custom callable if you need to get your link text in some way than just calling unicode on the object you're linking to.
I use it like this:
# the first 'blog' is the name of the model who's change page you want to link to
# the second is the name of the field on the model you're linking from
# so here, Post.blog is a foreign key to a Blog object.
#add_link_field('blog', 'blog')
class PostAdmin(admin.ModelAdmin):
inlines = [SubPostInline, DefinitionInline]
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
list_display = ('__unicode__', 'enabled', 'link')
# can call without arguments when you want to link to the model change page
# for the model of an inline model admin.
#add_link_field()
class PostInline(admin.StackedInline):
model = Post
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
extra = 0
Of course none of this would be necessary if I could nest the admin change views for SubPost and Definition inside the inline admin of Post on the Blog admin change page without patching Django.
I think that agf's solution is pretty awesome -- lots of kudos to him. But I needed a couple more features:
to be able to have multiple links for one admin
to be able to link to model in different app
Solution:
def add_link_field(target_model = None, field = '', app='', field_name='link',
link_text=unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Usage:
# 'apple' is name of model to link to
# 'fruit_food' is field name in `instance`, so instance.fruit_food = Apple()
# 'link2' will be name of this field
#add_link_field('apple','fruit_food',field_name='link2')
# 'cheese' is name of model to link to
# 'milk_food' is field name in `instance`, so instance.milk_food = Cheese()
# 'milk' is the name of the app where Cheese lives
#add_link_field('cheese','milk_food', 'milk')
class FoodAdmin(admin.ModelAdmin):
list_display = ("id", "...", 'link', 'link2')
I am sorry that the example is so illogical, but I didn't want to use my data.
I agree that its hard to do template editing so, I create a custom widget to show an anchor on the admin change view page(can be used on both forms and inline forms).
So, I used the anchor widget, along with form overriding to get the link on the page.
forms.py:
class AnchorWidget(forms.Widget):
def _format_value(self,value):
if self.is_localized:
return formats.localize_input(value)
return value
def render(self, name, value, attrs=None):
if not value:
value = u''
text = unicode("")
if self.attrs.has_key('text'):
text = self.attrs.pop('text')
final_attrs = self.build_attrs(attrs,name=name)
return mark_safe(u"<a %s>%s</a>" %(flatatt(final_attrs),unicode(text)))
class PostAdminForm(forms.ModelForm):
.......
def __init__(self,*args,**kwargs):
super(PostAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
if instance.blog:
href = reverse("admin:appname_Blog_change",args=(instance.blog))
self.fields["link"] = forms.CharField(label="View Blog",required=False,widget=AnchorWidget(attrs={'text':'go to blog','href':href}))
class BlogAdminForm(forms.ModelForm):
.......
link = forms..CharField(label="View Post",required=False,widget=AnchorWidget(attrs={'text':'go to post'}))
def __init__(self,*args,**kwargs):
super(BlogAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
href = ""
if instance:
posts = Post.objects.filter(blog=instance.pk)
for idx,post in enumerate(posts):
href = reverse("admin:appname_Post_change",args=(post["id"]))
self.fields["link_%s" % idx] = forms..CharField(label=Post["name"],required=False,widget=AnchorWidget(attrs={'text':post["desc"],'href':href}))
now in your ModelAdmin override the form attribute and you should get the desired result. I assumed you have a OneToOne relationship between these tables, If you have one to many then the BlogAdmin side will not work.
update:
I've made some changes to dynamically add links and that also solves the OneToMany issue with the Blog to Post hope this solves the issue. :)
After Pastebin:
In Your PostAdmin I noticed blog_link, that means your trying to show the blog link on changelist_view which lists all the posts. If I'm correct then you should add a method to show the link on the page.
class PostAdmin(admin.ModelAdmin):
model = Post
inlines = [SubPostInline, DefinitionInline]
list_display = ('__unicode__', 'enabled', 'blog_on_site')
def blog_on_site(self, obj):
href = reverse("admin:appname_Blog_change",args=(obj.blog))
return mark_safe(u"<a href='%s'>%s</a>" %(href,obj.desc))
blog_on_site.allow_tags = True
blog_on_site.short_description = 'Blog'
As far as the showing post links on BlogAdmin changelist_view you can do the same as above. My earlier solution will show you the link one level lower at the change_view page where you can edit each instance.
If you want the BlogAdmin page to show the links to the post in the change_view page then you will have to include each in the fieldsets dynamically by overriding the get_form method for class BlogAdmin and adding the link's dynamically, in get_form set the self.fieldsets, but first don't use tuples to for fieldsets instead use a list.
Based on agfs and SummerBreeze's suggestions, I've improved the decorator to handle unicode better and to be able to link to backwards-foreignkey fields (ManyRelatedManager with one result). Also you can now add a short_description as a list header:
from django.core.urlresolvers import reverse
from django.core.exceptions import MultipleObjectsReturned
from django.utils.safestring import mark_safe
def add_link_field(target_model=None, field='', app='', field_name='link',
link_text=unicode, short_description=None):
"""
decorator that automatically links to a model instance in the admin;
inspired by http://stackoverflow.com/questions/9919780/how-do-i-add-a-link-from-the-django-admin-page-of-one-object-
to-the-admin-page-o
:param target_model: modelname.lower or model
:param field: fieldname
:param app: appname
:param field_name: resulting field name
:param link_text: callback to link text function
:param short_description: list header
:return:
"""
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
# manyrelatedmanager with one result?
if link_obj.__class__.__name__ == "RelatedManager":
try:
link_obj = link_obj.get()
except MultipleObjectsReturned:
return u"multiple, can't link"
except link_obj.model.DoesNotExist:
return u""
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe(u"<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = short_description or (reverse_name + ' link')
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Edit: updated due to link being gone.
Looking through the source of the admin classes is enlightening: it shows that there is an object in context available to an admin view called "original".
Here is a similar situation, where I needed some info added to a change list view: Adding data to admin templates (on my blog).
I went to all the documentation, also I went to the IRC channel (BTW a great community) and they told me that is not possible to create a model and limit choices in a field where the 'current user' is in a ForeignKey.
I will try to explain this with an example:
class Project(models.Model):
name = models.CharField(max_length=100)
employees = models.ManyToManyField(Profile, limit_choices_to={'active': '1'})
class TimeWorked(models.Model):
project = models.ForeignKey(Project, limit_choices_to={'user': user})
hours = models.PositiveIntegerField()
Of course that code doesn't work because there is no 'user' object, but that was my idea and I was trying to send the object 'user' to the model to just limit the choices where the current user has projects, I don't want to see projects where I'm not in.
Thank you very much if you can help me or give me any advice, I don't want to you write all the app, just a tip how to deal with that. I have 2 days with this in my head and I can't figure it out :(
UPDATE: The solution is here: http://collingrady.wordpress.com/2008/07/24/useful-form-tricks-in-django/ sending request.user to a model.
This limiting of choices to current user is a kind of validation that needs to happen dynamically in the request cycle, not in the static Model definition.
In other words: at the point where you are creating an instance of this model you will be in a View and at that point you will have access to the current user and can limit the choices.
Then you just need a custom ModelForm to pass in the request.user to, see the example here:
http://collingrady.wordpress.com/2008/07/24/useful-form-tricks-in-django/
from datetime import datetime, timedelta
from django import forms
from mysite.models import Project, TimeWorked
class TimeWorkedForm(forms.ModelForm):
def __init__(self, user, *args, **kwargs):
super(ProjectForm, self).__init__(*args, **kwargs)
self.fields['project'].queryset = Project.objects.filter(user=user)
class Meta:
model = TimeWorked
then in your view:
def time_worked(request):
form = TimeWorkedForm(request.user, request.POST or None)
if form.is_valid():
obj = form.save()
# redirect somewhere
return render_to_response('time_worked.html', {'form': form})
Model itself doesn't know anything about current user but you can give this user in a view to the form which operates models objects (and in form reset choices for necessary field).
If you need this on admin site - you can try raw_id_admin along with django-granular-permissions (http://code.google.com/p/django-granular-permissions/ but I couldn't rapidly get it working on my django but it seems to be fresh enough for 1.0 so...).
At last, if you heavily need a selectbox in admin - then you'll need to hack django.contrib.admin itself.
Using class-based generic Views in Django 1.8.x / Python 2.7.x, here is what my colleagues and I came up with:
In models.py:
# ...
class Proposal(models.Model):
# ...
# Soft foreign key reference to customer
customer_id = models.PositiveIntegerField()
# ...
In forms.py:
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.forms import ModelForm, ChoiceField, Select
from django import forms
from django.forms.utils import ErrorList
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from .models import Proposal
from account.models import User
from customers.models import customer
def get_customers_by_user(curUser=None):
customerSet = None
# Users with userType '1' or '2' are superusers; they should be able to see
# all the customers regardless. Users with userType '3' or '4' are limited
# users; they should only be able to see the customers associated with them
# in the customized user admin.
#
# (I know, that's probably a terrible system, but it's one that I
# inherited, and am keeping for now.)
if curUser and (curUser.userType in ['1', '2']):
customerSet = customer.objects.all().order_by('company_name')
elif curUser:
customerSet = curUser.customers.all().order_by('company_name')
else:
customerSet = customer.objects.all().order_by('company_name')
return customerSet
def get_customer_choices(customerSet):
retVal = []
for customer in customerSet:
retVal.append((customer.customer_number, '%d: %s' % (customer.customer_number, customer.company_name)))
return tuple(retVal)
class CustomerFilterTestForm(ModelForm):
class Meta:
model = Proposal
fields = ['customer_id']
def __init__(self, user=None, *args, **kwargs):
super(CustomerFilterTestForm, self).__init__(*args, **kwargs)
self.fields['customer_id'].widget = Select(choices=get_customer_choices(get_customers_by_user(user)))
# ...
In views.py:
# ...
class CustomerFilterTestView(generic.UpdateView):
model = Proposal
form_class = CustomerFilterTestForm
template_name = 'proposals/customer_filter_test.html'
context_object_name = 'my_context'
success_url = "/proposals/"
def get_form_kwargs(self):
kwargs = super(CustomerFilterTestView, self).get_form_kwargs()
kwargs.update({
'user': self.request.user,
})
return kwargs
In templates/proposals/customer_filter_test.html:
{% extends "base/base.html" %}
{% block title_block %}
<title>Customer Filter Test</title>
{% endblock title_block %}
{% block header_add %}
<style>
label {
min-width: 300px;
}
</style>
{% endblock header_add %}
{% block content_body %}
<form action="" method="POST">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Save" class="btn btn-default" />
</form>
{% endblock content_body %}
I'm not sure that I fully understand exactly what you want to do, but I think that there's a good chance that you'll get at least part the way there using a custom Manager. In particular, don't try to define your models with restrictions to the current user, but create a manager that only returns objects that match the current user.
Hmmm, I don't fully understand your question. But if you can't do it when you declare the model maybe you can achieve the same thing with overriding methods of the class of objects where you "send" the user object, maybe start with the constructor.
Use threadlocals if you want to get current user that edits this model. Threadlocals middleware puts current user into process-wide variable. Take this middleware
from threading import local
_thread_locals = local()
def get_current_user():
return getattr(getattr(_thread_locals, 'user', None),'id',None)
class ThreadLocals(object):
"""Middleware that gets various objects from the
request object and saves them in thread local storage."""
def process_request(self, request):
_thread_locals.user = getattr(request, 'user', None)
Check the documentation on how to use middleware classes. Then anywhere in code you can call
user = threadlocals.get_current_user