magic-removal: fixed #1330: edit-inline works again on magic-removal. Note that the API will change *substantailly* before we're done (for example, this reintroduces core fields, which suck) but this at least gives us a place to start with.

Many many thanks for Christopher Lenz, my new hero.


git-svn-id: http://code.djangoproject.com/svn/django/branches/magic-removal@2502 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jacob Kaplan-Moss 2006-03-08 17:53:55 +00:00
parent 4e292dabc0
commit 738d9af1e8
8 changed files with 177 additions and 113 deletions

View File

@ -12,11 +12,5 @@
{% admin_field_line bound_field %} {% admin_field_line bound_field %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="item actions">
<button class="deletebutton" name="command" value="{{bound_related_object.relation.var_name}}.{{fcw.index}}.delete">Delete</button>
</div>
{% endfor %} {% endfor %}
<div class="collection actions"> </fieldset>
<button class="addbutton" name="command" value="{{bound_related_object.relation.var_name}}.add">Add</button>
</div>
</fieldset>

View File

@ -1,45 +1,43 @@
{% load admin_modify %} {% load admin_modify %}
<fieldset class="module">
<h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table>
<thead><tr>
{% for fw in bound_related_object.field_wrapper_list %}
{% if fw.needs_header %}
<th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst }}</th>
{% endif %}
{% endfor %}
{% for fcw in bound_related_object.form_field_collection_wrappers %}
{% if change %}{% if original_row_needed %}
{% if fcw.obj.original %}
<tr class="row-label {% cycle row1,row2 %}"><td colspan="{{ num_headers }}"><strong>{{ fcw.obj.original }}</strong></tr>
{% endif %}
{% endif %}{% endif %}
{% if fcw.obj.errors %}
<tr class="errorlist"><td colspan="{{ num_headers }}">
{{ fcw.obj.html_combined_error_list }}
</tr>
{% endif %}
<tr class="{% cycle row1,row2 %}">
{% for bound_field in fcw.bound_fields %}
{% if not bound_field.hidden %}
<td {{ bound_field.cell_class_attribute }}>
{% field_widget bound_field %}
</td>
{% endif %}
{% endfor %}
{% if bound_related_object.show_url %}<td>
{% if fcw.obj.original %}<a href="/r/{{ fcw.obj.original.content_type_id }}/{{ fcw.obj.original.id }}/">View on site</a>{% endif %}
</td>{% endif %}
</tr>
<fieldset class="module editinline"> {% endfor %} </table>
<h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2>
<table> {% for fcw in bound_related_object.form_field_collection_wrappers %}
<thead> {% for bound_field in fcw.bound_fields %}
<tr> {% if bound_field.hidden %}
{% for fw in bound_related_object.field_wrapper_list %} {% field_widget bound_field %}
{% if fw.needs_header %} {% endif %}
<th{{ fw.header_class_attribute }}>{{ fw.field.verbose_name|capfirst }}</th> {% endfor %}
{% endif %} {% endfor %}
{% endfor %} </fieldset>
<th>&nbsp;</th>
</tr>
</thead>
<tfoot>
<tr>
<td colspan='{{ num_headers }}'>
<button class="addlink" name="command" value="{{ bound_related_object.relation.var_name }}.add">
Add another {{ bound_related_object.relation.opts.verbose_name }}
</button>
</td>
</tr>
</tfoot>
<tbody>
{% for fcw in bound_related_object.form_field_collection_wrappers %}
<tr class="{% cycle row1,row2 %}{% if fcw.obj.errors %} error{% endif %}">
{% for bound_field in fcw.bound_fields %}
{% if not bound_field.hidden %}
<td {{ bound_field.cell_class_attribute }}>
{{ bound_field.html_error_list }}
{% field_widget bound_field %}
</td>
{% endif %}
{% endfor %}
<td class="controls" >
<button class="inline-deletelink" name="command" value="{{ bound_related_object.relation.var_name }}.{{ fcw.index }}.delete">
Delete {{ bound_related_object.relation.opts.verbose_name }}
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</fieldset>

View File

@ -954,6 +954,16 @@ def get_validation_errors(outfile, app=None):
except models.FieldDoesNotExist: except models.FieldDoesNotExist:
e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name) e.add(opts, '"ordering" refers to "%s", a field that doesn\'t exist.' % field_name)
# Check core=True, if needed.
for related in opts.get_followed_related_objects():
try:
for f in related.opts.fields:
if f.core:
raise StopIteration
e.add(related.opts, "At least one field in %s should have core=True, because it's being edited inline by %s.%s." % (related.opts.object_name, opts.module_name, opts.object_name))
except StopIteration:
pass
# Check unique_together. # Check unique_together.
for ut in opts.unique_together: for ut in opts.unique_together:
for field_name in ut: for field_name in ut:

View File

@ -74,7 +74,7 @@ class Field(object):
self.primary_key = primary_key self.primary_key = primary_key
self.maxlength, self.unique = maxlength, unique self.maxlength, self.unique = maxlength, unique
self.blank, self.null = blank, null self.blank, self.null = blank, null
self.rel, self.default = rel, default self.core, self.rel, self.default = core, rel, default
self.editable = editable self.editable = editable
self.validator_list = validator_list or [] self.validator_list = validator_list or []
self.prepopulate_from = prepopulate_from self.prepopulate_from = prepopulate_from
@ -88,10 +88,6 @@ class Field(object):
# Set db_index to True if the field has a relationship and doesn't explicitly set db_index. # Set db_index to True if the field has a relationship and doesn't explicitly set db_index.
self.db_index = db_index self.db_index = db_index
self.deprecated_args = []
if core:
self.deprecated_args.append('core')
# Increase the creation counter, and save our local copy. # Increase the creation counter, and save our local copy.
self.creation_counter = Field.creation_counter self.creation_counter = Field.creation_counter
Field.creation_counter += 1 Field.creation_counter += 1
@ -219,14 +215,29 @@ class Field(object):
params['validator_list'].append(curry(manipulator_validator_unique, self, opts, manipulator)) params['validator_list'].append(curry(manipulator_validator_unique, self, opts, manipulator))
# Only add is_required=True if the field cannot be blank. Primary keys # Only add is_required=True if the field cannot be blank. Primary keys
# are a special case. # are a special case, and fields in a related context should set this
params['is_required'] = not self.blank and not self.primary_key # as False, because they'll be caught by a separate validator --
# RequiredIfOtherFieldGiven.
params['is_required'] = not self.blank and not self.primary_key and not rel
# BooleanFields (CheckboxFields) are a special case. They don't take # BooleanFields (CheckboxFields) are a special case. They don't take
# is_required or validator_list. # is_required or validator_list.
if isinstance(self, BooleanField): if isinstance(self, BooleanField):
del params['validator_list'], params['is_required'] del params['validator_list'], params['is_required']
# If this field is in a related context, check whether any other fields
# in the related object have core=True. If so, add a validator --
# RequiredIfOtherFieldsGiven -- to this FormField.
if rel and not self.blank and not isinstance(self, AutoField) and not isinstance(self, FileField):
# First, get the core fields, if any.
core_field_names = []
for f in opts.fields:
if f.core and f != self:
core_field_names.extend(f.get_manipulator_field_names(name_prefix))
# Now, if there are any, add the validator to this FormField.
if core_field_names:
params['validator_list'].append(validators.RequiredIfOtherFieldsGiven(core_field_names, gettext_lazy("This field is required.")))
# Finally, add the field_names. # Finally, add the field_names.
field_names = self.get_manipulator_field_names(name_prefix) field_names = self.get_manipulator_field_names(name_prefix)
return [man(field_name=field_names[i], **params) for i, man in enumerate(field_objs)] return [man(field_name=field_names[i], **params) for i, man in enumerate(field_objs)]
@ -239,9 +250,8 @@ class Field(object):
Given the full new_data dictionary (from the manipulator), returns this Given the full new_data dictionary (from the manipulator), returns this
field's data. field's data.
""" """
#if rel: if rel:
# return new_data.get(self.name, [self.get_default()])[0] return new_data.get(self.name, [self.get_default()])[0]
#else:
val = new_data.get(self.name, self.get_default()) val = new_data.get(self.name, self.get_default())
if not self.empty_strings_allowed and val == '' and self.null: if not self.empty_strings_allowed and val == '' and self.null:
val = None val = None
@ -397,12 +407,12 @@ class DateTimeField(DateField):
def get_manipulator_new_data(self, new_data, rel=False): def get_manipulator_new_data(self, new_data, rel=False):
date_field, time_field = self.get_manipulator_field_names('') date_field, time_field = self.get_manipulator_field_names('')
#if rel: if rel:
# d = new_data.get(date_field, [None])[0] d = new_data.get(date_field, [None])[0]
# t = new_data.get(time_field, [None])[0] t = new_data.get(time_field, [None])[0]
#else: else:
d = new_data.get(date_field, None) d = new_data.get(date_field, None)
t = new_data.get(time_field, None) t = new_data.get(time_field, None)
if d is not None and t is not None: if d is not None and t is not None:
return datetime.datetime.combine(d, t) return datetime.datetime.combine(d, t)
return self.get_default() return self.get_default()
@ -492,7 +502,10 @@ class FileField(Field):
upload_field_name = self.get_manipulator_field_names('')[0] upload_field_name = self.get_manipulator_field_names('')[0]
if new_data.get(upload_field_name, False): if new_data.get(upload_field_name, False):
func = getattr(new_object, 'save_%s_file' % self.name) func = getattr(new_object, 'save_%s_file' % self.name)
func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"]) if rel:
func(new_data[upload_field_name][0]["filename"], new_data[upload_field_name][0]["content"])
else:
func(new_data[upload_field_name]["filename"], new_data[upload_field_name]["content"])
def get_directory_name(self): def get_directory_name(self):
return os.path.normpath(datetime.datetime.now().strftime(self.upload_to)) return os.path.normpath(datetime.datetime.now().strftime(self.upload_to))

View File

@ -418,6 +418,10 @@ class ForeignKey(RelatedField, Field):
kwargs['edit_inline'] = kwargs.pop('edit_inline_type') kwargs['edit_inline'] = kwargs.pop('edit_inline_type')
kwargs['rel'] = ManyToOne(to, to_field, kwargs['rel'] = ManyToOne(to, to_field,
num_in_admin=kwargs.pop('num_in_admin', 3),
min_num_in_admin=kwargs.pop('min_num_in_admin', None),
max_num_in_admin=kwargs.pop('max_num_in_admin', None),
num_extra_on_change=kwargs.pop('num_extra_on_change', 1),
edit_inline=kwargs.pop('edit_inline', False), edit_inline=kwargs.pop('edit_inline', False),
related_name=kwargs.pop('related_name', None), related_name=kwargs.pop('related_name', None),
limit_choices_to=kwargs.pop('limit_choices_to', None), limit_choices_to=kwargs.pop('limit_choices_to', None),
@ -427,10 +431,6 @@ class ForeignKey(RelatedField, Field):
self.db_index = True self.db_index = True
for name in ('num_in_admin', 'min_num_in_admin', 'max_num_in_admin', 'num_extra_on_change'):
if name in kwargs:
self.deprecated_args.append(name)
def get_attname(self): def get_attname(self):
return '%s_id' % self.name return '%s_id' % self.name
@ -501,6 +501,7 @@ class OneToOneField(RelatedField, IntegerField):
kwargs['edit_inline'] = kwargs.pop('edit_inline_type') kwargs['edit_inline'] = kwargs.pop('edit_inline_type')
kwargs['rel'] = OneToOne(to, to_field, kwargs['rel'] = OneToOne(to, to_field,
num_in_admin=kwargs.pop('num_in_admin', 0),
edit_inline=kwargs.pop('edit_inline', False), edit_inline=kwargs.pop('edit_inline', False),
related_name=kwargs.pop('related_name', None), related_name=kwargs.pop('related_name', None),
limit_choices_to=kwargs.pop('limit_choices_to', None), limit_choices_to=kwargs.pop('limit_choices_to', None),
@ -511,10 +512,6 @@ class OneToOneField(RelatedField, IntegerField):
self.db_index = True self.db_index = True
for name in ('num_in_admin',):
if name in kwargs:
self.deprecated_args.append(name)
def get_attname(self): def get_attname(self):
return '%s_id' % self.name return '%s_id' % self.name
@ -534,6 +531,7 @@ class ManyToManyField(RelatedField, Field):
def __init__(self, to, **kwargs): def __init__(self, to, **kwargs):
kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['verbose_name'] = kwargs.get('verbose_name', None)
kwargs['rel'] = ManyToMany(to, kwargs.pop('singular', None), kwargs['rel'] = ManyToMany(to, kwargs.pop('singular', None),
num_in_admin=kwargs.pop('num_in_admin', 0),
related_name=kwargs.pop('related_name', None), related_name=kwargs.pop('related_name', None),
filter_interface=kwargs.pop('filter_interface', None), filter_interface=kwargs.pop('filter_interface', None),
limit_choices_to=kwargs.pop('limit_choices_to', None), limit_choices_to=kwargs.pop('limit_choices_to', None),
@ -542,9 +540,6 @@ class ManyToManyField(RelatedField, Field):
if kwargs["rel"].raw_id_admin: if kwargs["rel"].raw_id_admin:
kwargs.setdefault("validator_list", []).append(self.isValidIDList) kwargs.setdefault("validator_list", []).append(self.isValidIDList)
Field.__init__(self, **kwargs) Field.__init__(self, **kwargs)
for name in ('num_in_admin'):
if name in kwargs:
self.deprecated_args.append(name)
if self.rel.raw_id_admin: if self.rel.raw_id_admin:
msg = gettext_lazy('Separate multiple IDs with commas.') msg = gettext_lazy('Separate multiple IDs with commas.')
@ -641,15 +636,17 @@ class ManyToManyField(RelatedField, Field):
pass pass
class ManyToOne: class ManyToOne:
def __init__(self, to, field_name, edit_inline=False, def __init__(self, to, field_name, num_in_admin=3, min_num_in_admin=None,
max_num_in_admin=None, num_extra_on_change=1, edit_inline=False,
related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False): related_name=None, limit_choices_to=None, lookup_overrides=None, raw_id_admin=False):
try: try:
to._meta to._meta
except AttributeError: except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT
assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, basestring), "'to' must be either a model, a model name or the string %r" % RECURSIVE_RELATIONSHIP_CONSTANT
self.to, self.field_name = to, field_name self.to, self.field_name = to, field_name
self.edit_inline = edit_inline self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
self.related_name = related_name self.min_num_in_admin, self.max_num_in_admin = min_num_in_admin, max_num_in_admin
self.num_extra_on_change, self.related_name = num_extra_on_change, related_name
self.limit_choices_to = limit_choices_to or {} self.limit_choices_to = limit_choices_to or {}
self.lookup_overrides = lookup_overrides or {} self.lookup_overrides = lookup_overrides or {}
self.raw_id_admin = raw_id_admin self.raw_id_admin = raw_id_admin
@ -660,22 +657,23 @@ class ManyToOne:
return self.to._meta.get_field(self.field_name) return self.to._meta.get_field(self.field_name)
class OneToOne(ManyToOne): class OneToOne(ManyToOne):
def __init__(self, to, field_name, edit_inline=False, def __init__(self, to, field_name, num_in_admin=0, edit_inline=False,
related_name=None, limit_choices_to=None, lookup_overrides=None, related_name=None, limit_choices_to=None, lookup_overrides=None,
raw_id_admin=False): raw_id_admin=False):
self.to, self.field_name = to, field_name self.to, self.field_name = to, field_name
self.edit_inline = edit_inline self.num_in_admin, self.edit_inline = num_in_admin, edit_inline
self.related_name = related_name self.related_name = related_name
self.limit_choices_to = limit_choices_to or {} self.limit_choices_to = limit_choices_to or {}
self.lookup_overrides = lookup_overrides or {} self.lookup_overrides = lookup_overrides or {}
self.raw_id_admin = raw_id_admin self.raw_id_admin = raw_id_admin
self.multiple = False self.multiple = False
class ManyToMany: class ManyToMany:
def __init__(self, to, singular=None, related_name=None, def __init__(self, to, singular=None, num_in_admin=0, related_name=None,
filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True): filter_interface=None, limit_choices_to=None, raw_id_admin=False, symmetrical=True):
self.to = to self.to = to
self.singular = singular or None self.singular = singular or None
self.num_in_admin = num_in_admin
self.related_name = related_name self.related_name = related_name
self.filter_interface = filter_interface self.filter_interface = filter_interface
self.limit_choices_to = limit_choices_to or {} self.limit_choices_to = limit_choices_to or {}

View File

@ -139,6 +139,9 @@ class AutomaticManipulator(forms.Manipulator):
if child_follow: if child_follow:
obj_list = expanded_data[related.var_name].items() obj_list = expanded_data[related.var_name].items()
if not obj_list:
continue
obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0]))) obj_list.sort(lambda x, y: cmp(int(x[0]), int(y[0])))
# For each related item... # For each related item...
@ -187,15 +190,8 @@ class AutomaticManipulator(forms.Manipulator):
if param != None: if param != None:
params[f.attname] = param params[f.attname] = param
# Related links are a special case, because we have to
# manually set the "content_type_id" and "object_id" fields.
if self.opts.has_related_links and related.opts.module_name == 'relatedlinks':
contenttypes_mod = get_module('core', 'contenttypes')
params['content_type_id'] = contenttypes_mod.get_object(package__label__exact=self.opts.app_label, python_module_name__exact=self.opts.module_name).id
params['object_id'] = new_object.id
# Create the related item. # Create the related item.
new_rel_obj = related.opts.get_model_module().Klass(**params) new_rel_obj = related.model(**params)
# If all the core fields were provided (non-empty), save the item. # If all the core fields were provided (non-empty), save the item.
if all_cores_given: if all_cores_given:

View File

@ -41,6 +41,35 @@ class RelatedObject(object):
""" """
return data # TODO return data # TODO
def get_list(self, parent_instance=None):
"Get the list of this type of object from an instance of the parent class."
if parent_instance is not None:
attr = getattr(parent_instance, self.get_accessor_name())
if self.field.rel.multiple:
# For many-to-many relationships, return a list of objects
# corresponding to the xxx_num_in_admin options of the field
objects = list(attr.all())
count = len(objects) + self.field.rel.num_extra_on_change
if self.field.rel.min_num_in_admin:
count = max(count, self.field.rel.min_num_in_admin)
if self.field.rel.max_num_in_admin:
count = min(count, self.field.rel.max_num_in_admin)
change = count - len(objects)
if change > 0:
return objects + [None for _ in range(change)]
if change < 0:
return objects[:change]
else: # Just right
return objects
else:
# A one-to-one relationship, so just return the single related
# object
return [attr]
else:
return [None for _ in range(self.field.rel.num_in_admin)]
def editable_fields(self): def editable_fields(self):
"Get the fields in this class that should be edited inline." "Get the fields in this class that should be edited inline."
return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field] return [f for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field]
@ -62,6 +91,30 @@ class RelatedObject(object):
over[self.field.name] = False over[self.field.name] = False
return self.opts.get_follow(over) return self.opts.get_follow(over)
def get_manipulator_fields(self, opts, manipulator, change, follow):
if self.field.rel.multiple:
if change:
attr = getattr(manipulator.original_object, self.get_accessor_name())
count = attr.count()
count += self.field.rel.num_extra_on_change
if self.field.rel.min_num_in_admin:
count = max(count, self.field.rel.min_num_in_admin)
if self.field.rel.max_num_in_admin:
count = min(count, self.field.rel.max_num_in_admin)
else:
count = self.field.rel.num_in_admin
else:
count = 1
fields = []
for i in range(count):
for f in self.opts.fields + self.opts.many_to_many:
if follow.get(f.name, False):
prefix = '%s.%d.' % (self.var_name, i)
fields.extend(f.get_manipulator_fields(self.opts, manipulator, change,
name_prefix=prefix, rel=True))
return fields
def __repr__(self): def __repr__(self):
return "<RelatedObject: %s related to %s>" % (self.name, self.field.name) return "<RelatedObject: %s related to %s>" % (self.name, self.field.name)

View File

@ -131,10 +131,10 @@ class FormWrapper:
def fill_inline_collections(self): def fill_inline_collections(self):
if not self._inline_collections: if not self._inline_collections:
ic = [] ic = []
children = self.manipulator.children.items() related_objects = self.manipulator.get_related_objects()
for rel_obj, child_manips in children: for rel_obj in related_objects:
data = rel_obj.extract_data(self.data) data = rel_obj.extract_data(self.data)
inline_collection = InlineObjectCollection(self.manipulator, rel_obj, child_manips, data, self.error_dict) inline_collection = InlineObjectCollection(self.manipulator, rel_obj, data, self.error_dict)
ic.append(inline_collection) ic.append(inline_collection)
self._inline_collections = ic self._inline_collections = ic
@ -213,12 +213,11 @@ class FormFieldCollection(FormFieldWrapper):
class InlineObjectCollection: class InlineObjectCollection:
"An object that acts like a sparse list of form field collections." "An object that acts like a sparse list of form field collections."
def __init__(self, parent_manipulator, rel_obj,child_manips, data, errors): def __init__(self, parent_manipulator, rel_obj, data, errors):
self.parent_manipulator = parent_manipulator self.parent_manipulator = parent_manipulator
self.rel_obj = rel_obj self.rel_obj = rel_obj
self.data = data self.data = data
self.errors = errors self.errors = errors
self.child_manips = child_manips
self._collections = None self._collections = None
self.name = rel_obj.name self.name = rel_obj.name
@ -240,7 +239,7 @@ class InlineObjectCollection:
def __iter__(self): def __iter__(self):
self.fill() self.fill()
return self._collections.values().__iter__() return iter(self._collections.values())
def items(self): def items(self):
self.fill() self.fill()
@ -250,22 +249,25 @@ class InlineObjectCollection:
if self._collections: if self._collections:
return return
else: else:
#var_name = self.rel_obj.opts.object_name.lower() var_name = self.rel_obj.opts.object_name.lower()
cols = {} collections = {}
#orig = hasattr(self.parent_manipulator, 'original_object') and self.parent_manipulator.original_object or None orig = None
#orig_list = self.rel_obj.get_list(orig) if hasattr(self.parent_manipulator, 'original_object'):
orig = self.parent_manipulator.original_object
orig_list = self.rel_obj.get_list(orig)
for i, manip in enumerate(self.child_manips) : for i, instance in enumerate(orig_list):
if manip and not manip.needs_deletion: collection = {'original': instance}
collection = {'original': manip.original_object} for f in self.rel_obj.editable_fields():
for field in manip.fields: for field_name in f.get_manipulator_field_names(''):
errors = self.errors.get(field.field_name, []) full_field_name = '%s.%d.%s' % (var_name, i, field_name)
field = self.parent_manipulator[full_field_name]
data = field.extract_data(self.data) data = field.extract_data(self.data)
last_part = field.field_name[field.field_name.rindex('.') + 1:] errors = self.errors.get(full_field_name, [])
collection[last_part] = FormFieldWrapper(field, data, errors) collection[field_name] = FormFieldWrapper(field, data, errors)
collections[i] = FormFieldCollection(collection)
self._collections = collections
cols[i] = FormFieldCollection(collection)
self._collections = cols
class FormField: class FormField:
"""Abstract class representing a form field. """Abstract class representing a form field.