diff --git a/django/conf/admin_media/js/urlify.js b/django/conf/admin_media/js/urlify.js index 412130ad6f..4a453ed9eb 100644 --- a/django/conf/admin_media/js/urlify.js +++ b/django/conf/admin_media/js/urlify.js @@ -10,7 +10,7 @@ function URLify(s, num_chars) { s = s.replace(r, ''); s = s.replace(/[^\w\s]/g, ''); // remove unneeded chars s = s.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces - s = s.replace(/\s+/g, '_'); // convert spaces to underscores + s = s.replace(/\s+/g, '-'); // convert spaces to dashes s = s.toLowerCase(); // convert to lowercase return s.substring(0, num_chars);// trim to first num_chars chars -} \ No newline at end of file +} diff --git a/django/conf/admin_templates/admin_change_form.html b/django/conf/admin_templates/admin_change_form.html new file mode 100644 index 0000000000..2beac80272 --- /dev/null +++ b/django/conf/admin_templates/admin_change_form.html @@ -0,0 +1,107 @@ +{% extends "base_site" %} +{% load admin_modify %} +{% load adminmedia %} +{% block extrahead %} + + {% for js in javascript_imports %} + {% include_admin_script js %} + {% endfor %} + +{% endblock %} + +{% block coltype %}{{ coltype }}{% endblock %} + +{% block bodyclass %}{{app_label}}-{{object_name.lower}} change-form{% endblock %} + +{% block breadcrumbs %}{% if not is_popup %} + +{% endif %} +{% endblock %} + +{% block content %}
+{% if change %} + {% if not is_popup %} + + {% endif %} +{% endif %} + +
+ +{% if is_popup %}{% endif %} + +{% if save_on_top %} + {% submit_row %} +{% endif %} + +{% if form.error_dict %} +

Please correct the error{{ form.error_dict.items|pluralize }} below.

+{% endif %} + + +{% for fieldset in admin_fieldsets %} +
+ {% if fieldset.name %} +

{{fieldset.name }}

+ {% endif %} + {% for bound_field_set in fieldset.bound_field_sets %} + {% for bound_field in bound_field_set %} + {% admin_field_bound bound_field %} + {% filter_interface_script_maybe bound_field %} + {% endfor %} + {% endfor %} +
+{% endfor %} + +{% if change %} + {% if ordered_objects %} +

Ordering

+
+ {% if form.order_.errors %}{{ form.order_.html_error_list }}{% endif %} +

{{ form.order_ }}

+
+ {% endif %} +{% endif %} + + +{% for relation in inline_related_objects %} + {% edit_inline relation %} +{% endfor %} + +{% submit_row %} + +{% if add %} + ' +{% endif %} + +{% if auto_populated_fields %} + +{% endif %} + +{% if change %} + {% if ordered_objects %} + {% if form.order_objects %}
') return '\n'.join(output) @@ -490,7 +601,7 @@ class FileUploadField(FormField): def render(self, data): return '' % \ - (FORM_FIELD_ID_PREFIX + self.field_name, self.__class__.__name__, + (self.get_id(), self.__class__.__name__, self.field_name) def html2python(data): diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 3ca42f14e6..e2070ab22e 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -146,6 +146,70 @@ class FieldDoesNotExist(Exception): class BadKeywordArguments(Exception): pass + +class InlineRelatedObject(object): + def __init__(self,parent_opts, opts, field): + self.parent_opts = parent_opts + self.opts = opts + self.field = field + self.name = opts.module_name + + def flatten_data(self,obj = None): + var_name = self.opts.object_name.lower() + new_data = {} + rel_instances = self.get_list(obj) + + for i, rel_instance in enumerate(rel_instances): + instance_data = {} + for f in self.opts.fields + self.opts.many_to_many: + field_data = f.flatten_data(rel_instance) + #if hasattr(f, 'editable') and f.editable and f != self.field: + for name, value in field_data.items(): + instance_data['%s.%d.%s' % (var_name, i, name)] = value + new_data.update(instance_data) + + return new_data + + def extract_data(self, data): + "Pull out the data meant for inline objects of this class, ie anything starting with our module name" + 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 != None: + func_name = 'get_%s_list' % self.parent_opts.get_rel_object_method_name(self.opts, self.field) + func = getattr(parent_instance, func_name) + list = func() + + count = len(list) + 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(list) + if change > 0: + return list + [None for _ in range(change)] + if change < 0: + return list[:change] + else: # Just right + return list + else: + return [None for _ in range(self.field.rel.num_in_admin)] + + + def editable_fields(self, wrapping_func = lambda x: x): + """Get the fields in this class that should be edited inline. + Pass a callable, eg a class, as the second argument to wrap the fields. + This can be useful to add extra attributes for use in templates.""" + + return [wrapping_func(f) for f in self.opts.fields + self.opts.many_to_many if f.editable and f != self.field ] + + def __repr__(self): + return "" % ( self.name, self.field.name) + + + class Options: def __init__(self, module_name='', verbose_name='', verbose_name_plural='', db_table='', fields=None, ordering=None, unique_together=None, admin=None, has_related_links=False, @@ -317,6 +381,12 @@ class Options: def get_inline_related_objects(self): return [(a, b) for a, b in self.get_all_related_objects() if b.rel.edit_inline] + def get_inline_related_objects_wrapped(self): + return [InlineRelatedObject(self, opts, field) for opts, field in self.get_all_related_objects() if field.rel.edit_inline] + + def get_data_holders(self): + return self.fields + self.many_to_many + self.get_inline_related_objects_wrapped() + def get_all_related_many_to_many_objects(self): module_list = get_installed_model_modules() rel_objs = [] @@ -594,6 +664,7 @@ class ModelBase(type): new_mod.get_latest = curry(function_get_latest, opts, new_class, does_not_exist_exception) for f in opts.fields: + #TODO : change this into a virtual function so that user defined fields will be able to add methods to module or class. if f.choices: # Add "get_thingie_display" method to get human-readable value. func = curry(method_get_display_value, f) @@ -788,8 +859,14 @@ def method_save(opts, self): # If it does already exist, do an UPDATE. if cursor.fetchone(): db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.column), False)) for f in non_pks] - cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % (opts.db_table, - ','.join(['%s=%%s' % f.column for f in non_pks]), opts.pk.column), + while 1: + try: + idx = db_values.index('') + non_pks[idx:idx+1] = [] + db_values[idx:idx +1] = [] + except: break + cursor.execute("UPDATE %s SET %s WHERE %s=%%s" % (opts.db_table, + ','.join(['%s=%%s' % f.column for f in non_pks]), opts.pk.column), db_values + [pk_val]) else: record_exists = False @@ -1332,16 +1409,21 @@ def function_get_sql_clause(opts, **kwargs): if f == '?': # Special case. order_by.append(db.get_random_function_sql()) else: + if f.startswith('-'): + col_name = f[1:] + order = "DESC" + else: + col_name = f + order = "ASC" # Use the database table as a column prefix if it wasn't given, # and if the requested column isn't a custom SELECT. - if "." not in f and f not in [k[0] for k in kwargs.get('select', [])]: + if "." not in col_name and col_name not in [k[0] for k in kwargs.get('select', [])]: table_prefix = opts.db_table + '.' else: table_prefix = '' - if f.startswith('-'): - order_by.append('%s%s DESC' % (table_prefix, orderfield2column(f[1:], opts))) - else: - order_by.append('%s%s ASC' % (table_prefix, orderfield2column(f, opts))) + + order_by.append('%s%s %s' % (table_prefix, orderfield2column(col_name, opts), order)) + order_by = ", ".join(order_by) # LIMIT and OFFSET clauses @@ -1397,6 +1479,8 @@ def get_manipulator(opts, klass, extra_methods, add=False, change=False): man.__module__ = MODEL_PREFIX + '.' + opts.module_name # Set this explicitly, as above. man.__init__ = curry(manipulator_init, opts, add, change) man.save = curry(manipulator_save, opts, klass, add, change) + man.get_inline_related_objects_wrapped = curry(manipulator_get_inline_related_objects_wrapped, opts, klass, add, change) + man.flatten_data = curry(manipulator_flatten_data, opts, klass, add, change) for field_name_list in opts.unique_together: setattr(man, 'isUnique%s' % '_'.join(field_name_list), curry(manipulator_validator_unique_together, field_name_list, opts)) for f in opts.fields: @@ -1439,20 +1523,20 @@ def manipulator_init(opts, add, change, self, obj_key=None): self.fields.extend(f.get_manipulator_fields(opts, self, change)) # Add fields for related objects. - for rel_opts, rel_field in opts.get_inline_related_objects(): + for obj in opts.get_inline_related_objects_wrapped(): if change: - count = getattr(self.original_object, 'get_%s_count' % opts.get_rel_object_method_name(rel_opts, rel_field))() - count += rel_field.rel.num_extra_on_change - if rel_field.rel.min_num_in_admin: - count = max(count, rel_field.rel.min_num_in_admin) - if rel_field.rel.max_num_in_admin: - count = min(count, rel_field.rel.max_num_in_admin) + count = getattr(self.original_object, 'get_%s_count' % opts.get_rel_object_method_name(obj.opts, obj.field))() + count += obj.field.rel.num_extra_on_change + if obj.field.rel.min_num_in_admin: + count = max(count, obj.field.rel.min_num_in_admin) + if obj.field.rel.max_num_in_admin: + count = min(count, obj.field.rel.max_num_in_admin) else: - count = rel_field.rel.num_in_admin - for f in rel_opts.fields + rel_opts.many_to_many: - if f.editable and f != rel_field and (not f.primary_key or (f.primary_key and change)): + count = obj.field.rel.num_in_admin + for f in obj.opts.fields + obj.opts.many_to_many: + if f.editable and f != obj.field : for i in range(count): - self.fields.extend(f.get_manipulator_fields(rel_opts, self, change, name_prefix='%s.%d.' % (rel_opts.object_name.lower(), i), rel=True)) + self.fields.extend(f.get_manipulator_fields(obj.opts, self, change, name_prefix='%s.%d.' % (obj.opts.object_name.lower(), i), rel=True)) # Add field for ordering. if change and opts.get_ordered_objects(): @@ -1593,6 +1677,16 @@ def manipulator_save(opts, klass, add, change, self, new_data): getattr(new_object, 'set_%s_order' % rel_opts.object_name.lower())(order) return new_object +def manipulator_get_inline_related_objects_wrapped(opts, klass, add, change, self): + return opts.get_inline_related_objects_wrapped() + +def manipulator_flatten_data(opts, klass, add, change, self): + new_data = {} + obj = change and self.original_object or None + for f in opts.get_data_holders(): + new_data.update(f.flatten_data(obj)) + return new_data + def manipulator_validator_unique_together(field_name_list, opts, self, field_data, all_data): from django.utils.text import get_text_list field_list = [opts.get_field(field_name) for field_name in field_name_list] diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 3e5cca169d..edc6093b67 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -16,7 +16,7 @@ BLANK_CHOICE_DASH = [("", "---------")] BLANK_CHOICE_NONE = [("", "None")] # Values for Relation.edit_inline. -TABULAR, STACKED = 1, 2 +TABULAR, STACKED = "admin_edit_inline_tabular", "admin_edit_inline_stacked" RECURSIVE_RELATIONSHIP_CONSTANT = 'self' @@ -155,7 +155,7 @@ class Field(object): if hasattr(self.default, '__get_value__'): return self.default.__get_value__() return self.default - if self.null: + if not self.empty_strings_allowed or self.null: return None return "" @@ -163,7 +163,7 @@ class Field(object): """ Returns a list of field names that this object adds to the manipulator. """ - return [name_prefix + self.name] + return [name_prefix + self.column] def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): """ @@ -177,28 +177,28 @@ class Field(object): if self.maxlength and not self.choices: # Don't give SelectFields a maxlength parameter. params['maxlength'] = self.maxlength if isinstance(self.rel, ManyToOne): + params['member_name'] = name_prefix + self.get_db_column() if self.rel.raw_id_admin: field_objs = self.get_manipulator_field_objs() params['validator_list'].append(curry(manipulator_valid_rel_key, self, manipulator)) else: if self.radio_admin: field_objs = [formfields.RadioSelectField] - params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) params['ul_class'] = get_ul_class(self.radio_admin) else: if self.null: field_objs = [formfields.NullSelectField] else: field_objs = [formfields.SelectField] - params['choices'] = self.get_choices() + params['choices'] = self.get_choices_default() elif self.choices: if self.radio_admin: field_objs = [formfields.RadioSelectField] - params['choices'] = self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) params['ul_class'] = get_ul_class(self.radio_admin) else: field_objs = [formfields.SelectField] - params['choices'] = self.get_choices() + + params['choices'] = self.get_choices_default() else: field_objs = self.get_manipulator_field_objs() @@ -258,14 +258,40 @@ class Field(object): val = None return val + def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH): "Returns a list of tuples used as SelectField choices for this field." + first_choice = include_blank and blank_choice or [] if self.choices: return first_choice + list(self.choices) rel_obj = self.rel.to - return first_choice + [(getattr(x, rel_obj.pk.column), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)] + choices = first_choice + [(getattr(x, rel_obj.pk.column), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_to)] + + return choices + + def get_choices_default(self): + if(self.radio_admin): + return self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE) + else: + return self.get_choices() + + def _get_val_from_obj(self, obj): + if obj: + return getattr(obj, self.column) + else: + return self.get_default() + + def flatten_data(self, obj = None): + """ + Returns a dictionary mapping the field's manipulator field names to its + "flattened" string values for the admin view. Obj is the instance to extract the + values from. + """ + return { self.get_db_column(): self._get_val_from_obj(obj)} + + class AutoField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): @@ -274,7 +300,7 @@ class AutoField(Field): def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): if not rel: - return [] # Don't add a FormField unless it's in a related context. + return [] # Don't add a FormField unless it's in a related change context. return Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel) def get_manipulator_field_objs(self): @@ -330,6 +356,10 @@ class DateField(Field): def get_manipulator_field_objs(self): return [formfields.DateField] + def flatten_data(self, obj = None): + val = self._get_val_from_obj(obj) + return {self.get_db_column(): (val is not None and val.strftime("%Y-%m-%d") or '')} + class DateTimeField(DateField): def get_db_prep_save(self, value): # Casts dates into string format for entry into database. @@ -359,6 +389,12 @@ class DateTimeField(DateField): return datetime.datetime.combine(d, t) return self.get_default() + def flatten_data(self,obj = None): + val = self._get_val_from_obj(obj) + date_field, time_field = self.get_manipulator_field_names('') + return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), + time_field: (val is not None and val.strftime("%H:%M:%S") or '')} + class EmailField(Field): def get_manipulator_field_objs(self): return [formfields.EmailField] @@ -542,6 +578,10 @@ class TimeField(Field): def get_manipulator_field_objs(self): return [formfields.TimeField] + def flatten_data(self,obj = None): + val = self._get_val_from_obj(obj) + return {self.get_db_column(): (val is not None and val.strftime("%H:%M:%S") or '')} + class URLField(Field): def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): if verify_exists: @@ -598,6 +638,18 @@ class ForeignKey(Field): def get_manipulator_field_objs(self): return [formfields.IntegerField] + def flatten_data(self, obj = None): + if not obj: + # In required many-to-one fields with only one available choice, + # select that one available choice. Note: We have to check that + # the length of choices is *2*, not 1, because SelectFields always + # have an initial "blank" value. + if not self.blank and not self.rel.raw_id_admin and self.choices: + choice_list = self.get_choices_default() + if len(choice_list) == 2: + return { self.name : choice_list[1][0] } + return Field.flatten_data(self, obj) + class ManyToManyField(Field): def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural) @@ -615,9 +667,12 @@ class ManyToManyField(Field): if self.rel.raw_id_admin: return [formfields.CommaSeparatedIntegerField] else: - choices = self.get_choices(include_blank=False) + choices = self.get_choices_default() return [curry(formfields.SelectMultipleField, size=min(max(len(choices), 5), 15), choices=choices)] + def get_choices_default(self): + return Field.get_choices(self, include_blank=False) + def get_m2m_db_table(self, original_opts): "Returns the name of the many-to-many 'join' table." return '%s_%s' % (original_opts.db_table, self.name) @@ -638,6 +693,25 @@ class ManyToManyField(Field): len(badkeys) == 1 and badkeys[0] or tuple(badkeys), len(badkeys) == 1 and "is" or "are") + def flatten_data(self, obj = None): + new_data = {} + if obj: + get_list_func = getattr(obj, 'get_%s_list' % self.rel.singular) + instance_ids = [getattr(instance, self.rel.to.pk.column) for instance in get_list_func()] + if self.rel.raw_id_admin: + new_data[self.name] = ",".join([str(id) for id in instance_ids]) + elif not self.rel.edit_inline: + new_data[self.name] = instance_ids + else: + # In required many-to-many fields with only one available choice, + # select that one available choice. + if not self.blank and not self.rel.edit_inline and not self.rel.raw_id_admin and self.choices: + choice_list = self.get_choices_default() + if len(choice_list) == 1: + new_data[self.name] = [choices_list[0][0]] + return new_data + + class OneToOneField(IntegerField): def __init__(self, to, to_field=None, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', 'ID') @@ -720,6 +794,12 @@ class Admin: Returns self.fields, except with fields as Field objects instead of field names. If self.fields is None, defaults to putting every non-AutoField field with editable=True in a single fieldset. + + returns a list of lists of name, dict + the dict has attribs 'fields' and maybe 'classes'. + fields is a list of subclasses of Field. + + Return value needs to be encapsulated. """ if self.fields is None: field_struct = ((None, {'fields': [f.name for f in opts.fields + opts.many_to_many if f.editable and not isinstance(f, AutoField)]}),) diff --git a/django/core/validators.py b/django/core/validators.py index 697c2efe81..d8b0097e9f 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -12,7 +12,7 @@ import re _datere = r'\d{4}-((?:0?[1-9])|(?:1[0-2]))-((?:0?[1-9])|(?:[12][0-9])|(?:3[0-1]))' _timere = r'(?:[01]?[0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?' -alnum_re = re.compile(r'^\w+$') +alnum_re = re.compile(r'^[\w-]+$') alnumurl_re = re.compile(r'^[\w/]+$') ansi_date_re = re.compile('^%s$' % _datere) ansi_time_re = re.compile('^%s$' % _timere) @@ -53,7 +53,7 @@ class CriticalValidationError(Exception): def isAlphaNumeric(field_data, all_data): if not alnum_re.search(field_data): - raise ValidationError, "This value must contain only letters, numbers and underscores." + raise ValidationError, "This value must contain only letters, numbers, dashes and underscores." def isAlphaNumericURL(field_data, all_data): if not alnumurl_re.search(field_data): diff --git a/django/templatetags/admin_modify.py b/django/templatetags/admin_modify.py new file mode 100644 index 0000000000..687306a8b5 --- /dev/null +++ b/django/templatetags/admin_modify.py @@ -0,0 +1,260 @@ +from django.core import template, template_loader, meta +from django.conf.settings import ADMIN_MEDIA_PREFIX +from django.utils.text import capfirst +from django.utils.html import escape +from django.utils.functional import curry + +from django.views.admin.main import AdminBoundField +import re + +class IncludeAdminScriptNode(template.Node): + def __init__(self, var): + self.var = var + + def render(self, context): + resolved = template.resolve_variable(self.var, context) + return '' % \ + (ADMIN_MEDIA_PREFIX, resolved) + +class SubmitRowNode(template.Node): + def __init__(self): + pass + + def render(self, context): + change = context['change'] + add = context['add'] + show_delete = context['show_delete'] + ordered_objects = context['ordered_objects'] + save_as = context['save_as'] + has_delete_permission = context['has_delete_permission'] + is_popup = context['is_popup'] + + t = ['
'] + onclick_attrib = ordered_objects and change and 'onclick="submitOrderForm();"' or '' + + if not is_popup: + if has_delete_permission and (change or show_delete): + t.append('

Delete

') + if change and save_as: + t.append('' % onclick_attrib) + if (not save_as or add): + t.append('' % onclick_attrib) + t.append('' % onclick_attrib ) + t.append('' % onclick_attrib) + t.append('
\n') + + return ''.join(t) + + + + +class AdminFieldBoundNode(template.Node): + def __init__(self, argument): + self.argument = argument + + def render(self, context): + argument_val = template.resolve_variable(self.argument, context) + if (isinstance(argument_val, list)): + bound_fields = argument_val + else: + bound_fields = [argument_val] + add = context['add'] + change = context['change'] + + context.push() + context['bound_fields'] = bound_fields + context['class_names'] = " ".join(self.get_class_names(bound_fields)) + t = template_loader.get_template("admin_field") + output = t.render(context) + context.pop() + + return output + + def get_class_names(self, bound_fields): + + class_names = ['form-row'] + for bound_field in bound_fields: + for f in bound_field.form_fields: + if f.errors(): + class_names.append('errors') + break + + # Assumes BooleanFields won't be stacked next to each other! + if isinstance(bound_fields[0].field, meta.BooleanField): + class_names.append('checkbox-row') + + return class_names + +class FieldWidgetNode(template.Node): + def __init__(self, bound_field_var): + self.bound_field_var = bound_field_var + + def render(self, context): + bound_field = template.resolve_variable(self.bound_field_var, context) + add = context['add'] + change = context['change'] + + context.push() + context['bound_field'] = bound_field + t = template_loader.get_template("admin_field_widget") + output = t.render(context) + context.pop() + + return output + + + +class FieldWrapper(object): + def __init__(self, field ): + self.field = field + + def needs_header(self): + return not isinstance(self.field, meta.AutoField) + + def header_class_attribute(self): + return self.field.blank and ' class="optional"' or '' + + def use_raw_id_admin(self): + return isinstance(self.field.rel, (meta.ManyToOne, meta.ManyToMany)) \ + and self.field.rel.raw_id_admin + +class FormFieldCollectionWrapper(object): + def __init__(self, obj, fields): + self.obj = obj + self.fields = fields + self.bound_fields = [ AdminBoundField(field, obj['original'], True, self.obj) for field in self.fields ] + + def showurl(self): + return False + +class EditInlineNode(template.Node): + def __init__(self, rel_var): + self.rel_var = rel_var + + def render(self, context): + relation = template.resolve_variable(self.rel_var, context) + add, change = context['add'], context['change'] + + context.push() + + self.fill_context(relation, add, change, context) + + t = template_loader.get_template(relation.field.rel.edit_inline) + + output = t.render(context) + + context.pop() + return output + + + def fill_context(self, relation, add, change, context): + field_wrapper_list = relation.editable_fields(FieldWrapper) + + var_name = relation.opts.object_name.lower() + + form = template.resolve_variable('form', context) + form_field_collections = form[relation.opts.module_name] + fields = relation.editable_fields() + form_field_collection_wrapper_list = [FormFieldCollectionWrapper(o,fields) for o in form_field_collections] + + context['field_wrapper_list'] = field_wrapper_list + context['form_field_collection_wrapper_list'] = form_field_collection_wrapper_list + context['num_headers'] = len(field_wrapper_list) + context['original_row_needed'] = max([fw.use_raw_id_admin() for fw in field_wrapper_list]) +# context['name_prefix'] = "%s." % (var_name,) + +class FieldLabelNode(template.Node): + def __init__(self, bound_field_var): + self.bound_field_var = bound_field_var + + def render(self, context): + bound_field = template.resolve_variable(self.bound_field_var, context) + class_names = [] + if isinstance(bound_field.field, meta.BooleanField): + class_names.append("vCheckboxLabel") + else: + if not bound_field.field.blank: + class_names.append('required') + if not bound_field.first: + class_names.append('inline') + + class_str = class_names and ' class="%s"' % ' '.join(class_names) or '' + return ' ' % (bound_field.element_id, class_str, capfirst(bound_field.field.verbose_name) ) + +class OutputAllNode(template.Node): + def __init__(self, form_fields_var): + self.form_fields_var = form_fields_var + + def render(self, context): + form_fields = template.resolve_variable(self.form_fields_var, context) + return ''.join([str(f) for f in form_fields]) + +class AutoPopulatedFieldScriptNode(template.Node): + def __init__(self, auto_pop_var): + self.auto_pop_var = auto_pop_var + + def render(self,context): + auto_pop_fields = template.resolve_variable(self.auto_pop_var, context) + change = context['change'] + for field in auto_pop_fields: + t = [] + if change: + t.append('document.getElementById("id_%s")._changed = true;' % field.name ) + else: + t.append('document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name) + + add_values = ' + " " + '.join(['document.getElementById("id_%s").value' % g for g in field.prepopulate_from]) + for f in field.prepopulate_from: + t.append('document.getElementById("id_%s").onkeyup = function() { var e = document.getElementById("id_%s"); if(e._changed) { e.value = URLify(%s, %s);} } ' % (f, field.name, add_values, field.maxlength) ) + + return ''.join(t) + +class FilterInterfaceScriptMaybeNode(template.Node): + def __init__(self, bound_field_var): + self.bound_field_var = bound_field_var + + def render(self, context): + bound_field = template.resolve_variable(self.bound_field_var, context) + f = bound_field.field + if f.rel and isinstance(f.rel, meta.ManyToMany) and f.rel.filter_interface: + return '\n' % (f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX) + else: + return '' + + + + +def do_submit_row(parser, token): + return SubmitRowNode() + + +def do_one_arg_tag(node_factory, parser,token): + tokens = token.contents.split() + if len(tokens) != 2: + raise template.TemplateSyntaxError("%s takes 1 argument" % tokens[0]) + return node_factory(tokens[1]) + + +one_arg_tag_nodes = [ + IncludeAdminScriptNode, + AdminFieldBoundNode, + FieldLabelNode, + FieldWidgetNode, + OutputAllNode, + EditInlineNode, + AutoPopulatedFieldScriptNode, + FilterInterfaceScriptMaybeNode, +] + +word = re.compile('[A-Z][a-z]+') +def register_one_arg_tag(node): + tag_name = '_'.join([ s.lower() for s in word.findall(node.__name__)[:-1] ]) + parse_func = curry(do_one_arg_tag, node) + template.register_tag(tag_name, parse_func) + + + +for node in one_arg_tag_nodes: + register_one_arg_tag(node) + +template.register_tag('submit_row', do_submit_row ) diff --git a/django/views/admin/main.py b/django/views/admin/main.py index a6109a46cd..7222faf455 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -1,6 +1,6 @@ # Generic admin views, with admin templates created dynamically at runtime. -from django.core import formfields, meta, template_loader +from django.core import formfields, meta, template_loader, template from django.core.exceptions import Http404, ObjectDoesNotExist, PermissionDenied from django.core.extensions import DjangoContext as Context from django.core.extensions import get_object_or_404, render_to_response @@ -493,23 +493,6 @@ def change_list(request, app_label, module_name): }) return HttpResponse(t.render(c)) -def _get_flattened_data(field, val): - """ - Returns a dictionary mapping the field's manipulator field names to its - "flattened" string values for the admin view. "val" is an instance of the - field's value. - """ - if isinstance(field, meta.DateTimeField): - date_field, time_field = field.get_manipulator_field_names('') - return {date_field: (val is not None and val.strftime("%Y-%m-%d") or ''), - time_field: (val is not None and val.strftime("%H:%M:%S") or '')} - elif isinstance(field, meta.DateField): - return {field.name: (val is not None and val.strftime("%Y-%m-%d") or '')} - elif isinstance(field, meta.TimeField): - return {field.name: (val is not None and val.strftime("%H:%M:%S") or '')} - else: - return {field.name: val} - use_raw_id_admin = lambda field: isinstance(field.rel, (meta.ManyToOne, meta.ManyToMany)) and field.rel.raw_id_admin def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects): @@ -530,6 +513,327 @@ def _get_submit_row_template(opts, app_label, add, change, show_delete, ordered_ t.append('
\n') return t +def get_javascript_imports(opts,auto_populated_fields, ordered_objects, admin_field_objs): +# Put in any necessary JavaScript imports. + js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] + if auto_populated_fields: + js.append('js/urlify.js') + if opts.has_field_type(meta.DateTimeField) or opts.has_field_type(meta.TimeField) or opts.has_field_type(meta.DateField): + js.extend(['js/calendar.js', 'js/admin/DateTimeShortcuts.js']) + if ordered_objects: + js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) + if opts.admin.js: + js.extend(opts.admin.js) + seen_collapse = False + for _, options in admin_field_objs: + if not seen_collapse and 'collapse' in options.get('classes', ''): + seen_collapse = True + js.append('js/admin/CollapsedFieldsets.js' ) + try: + for field_list in options['fields']: + for f in field_list: + if f.rel and isinstance(f, meta.ManyToManyField) and f.rel.filter_interface: + js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) + raise StopIteration + except StopIteration: + break + return js + +class BoundField(object): + def __init__(self, field, original, rel, field_mapping): + self.field = field + self.form_fields = self.resolve_form_fields(field_mapping) + self.original = original + self.rel = rel + + def resolve_form_fields(self, field_mapping): + return [field_mapping[name] for name in self.field.get_manipulator_field_names('')] + + def as_field_list(self): + return [self.field] + + def original_value(self): + return self.original.__dict__[self.field.name] + +class AdminBoundField(BoundField): + def __init__(self, field, original, rel, field_mapping): + super(AdminBoundField, self).__init__(field,original, rel, field_mapping) + + self.element_id = self.form_fields[0].get_id() + self.has_label_first = not isinstance(self.field, meta.BooleanField) + self.raw_id_admin = use_raw_id_admin(field) + self.is_date_time = isinstance(field, meta.DateTimeField) + self.is_file_field = isinstance(field, meta.FileField) + self.needs_add_label = field.rel and isinstance(field.rel, meta.ManyToOne) or isinstance(field.rel, meta.ManyToMany) and field.rel.to.admin + self.not_in_table = isinstance(self.field, meta.AutoField) + self.first = True + + classes = [] + if(self.raw_id_admin): + classes.append('nowrap') + if max([bool(f.errors()) for f in self.form_fields]): + classes.append('error') + self.cell_class_attribute = ' '.join(classes) + self._repr_filled = False + + + + def _fetch_existing_repr(self, func_name): + class_dict = self.original.__class__.__dict__ + func = class_dict.get(func_name) + return func(self.original) + + def _fill_existing_repr(self): + if self._repr_filled: + return + #HACK + if isinstance(self.field.rel, meta.ManyToOne): + func_name = 'get_%s' % self.field.name + self._repr = self._fetch_existing_repr(func_name) + elif isinstance(self.field.rel, meta.ManyToMany): + func_name = 'get_%s_list' % self.field.name + self._repr = ",".join(self._fetch_existing_repr(func_name)) + self._repr_filled = True + + def existing_repr(self): + self._fill_existing_repr() + return self._repr + + def __repr__(self): + return repr(self.__dict__) + + def html_error_list(self): + return " ".join([form_field.html_error_list() for form_field in self.form_fields if form_field.errors]) + + +class AdminFieldSet(object): + def __init__(self, fieldset_name, options, form, original): + self.name = fieldset_name + self.options = options + self.bound_field_sets = self.get_bound_field_sets(form, original) + self.classes = options.get('classes', '') + + def __repr__(self): + return "Fieldset:(%s,%s)" % (self.name, self.bound_field_sets) + + def get_bound_field_sets(self, form, original): + fields = self.options['fields'] + bound_field_sets = [ [AdminBoundField(f, original, False, form) for f in field ] for field in fields] + for set in bound_field_sets: + first = True + for bound_field in set: + bound_field.first = first + first = False + + return bound_field_sets + +def fill_extra_context(opts, app_label, context, add=False, change=False, show_delete=False, form_url=''): + admin_field_objs = opts.admin.get_field_objs(opts) + ordered_objects = opts.get_ordered_objects()[:] + auto_populated_fields = [f for f in opts.fields if f.prepopulate_from] + + javascript_imports = get_javascript_imports(opts,auto_populated_fields, ordered_objects, admin_field_objs); + + if ordered_objects: + coltype = 'colMS' + else: + coltype = 'colM' + + has_absolute_url = hasattr(opts.get_model_module().Klass, 'get_absolute_url') + + form_enc_attrib = opts.has_field_type(meta.FileField) and 'enctype="multipart/form-data" ' or '' + + form = context['form'] + original = context['original'] + admin_fieldsets = [AdminFieldSet(name, options, form, original) for name, options in admin_field_objs] + inline_related_objects = opts.get_inline_related_objects_wrapped() + + ordered_object_names = ' '.join(['object.%s' % o.pk.name for o in ordered_objects]) + + extra_context = { + 'add': add, + 'change': change, + 'admin_field_objs' : admin_field_objs, + 'ordered_objects' : ordered_objects, + 'auto_populated_fields' : auto_populated_fields, + 'javascript_imports' : javascript_imports, + 'coltype' : coltype, + 'has_absolute_url': has_absolute_url, + 'form_enc_attrib': form_enc_attrib, + 'form_url' : form_url, + 'admin_fieldsets' : admin_fieldsets, + 'inline_related_objects': inline_related_objects, + 'ordered_object_names' : ordered_object_names, + 'content_type_id' : opts.get_content_type_id(), + 'save_on_top' : opts.admin.save_on_top, + 'verbose_name_plural': opts.verbose_name_plural, + 'save_as': opts.admin.save_as, + 'app_label': app_label, + 'object_name': opts.object_name, + 'has_delete_permission' : context['perms'][app_label][opts.get_delete_permission()] + } + + context.update(extra_context) + + +def add_stage_new(request, app_label, module_name, show_delete=False, form_url='', post_url='../', post_url_continue='../%s/', object_id_override=None): + mod, opts = _get_mod_opts(app_label, module_name) + if not request.user.has_perm(app_label + '.' + opts.get_add_permission()): + raise PermissionDenied + manipulator = mod.AddManipulator() + if request.POST: + new_data = request.POST.copy() + if opts.has_field_type(meta.FileField): + new_data.update(request.FILES) + errors = manipulator.get_validation_errors(new_data) + if not errors and not request.POST.has_key("_preview"): + for f in opts.many_to_many: + if f.rel.raw_id_admin: + new_data.setlist(f.name, new_data[f.name].split(",")) + manipulator.do_html2python(new_data) + new_object = manipulator.save(new_data) + pk_value = getattr(new_object, opts.pk.column) + log.log_action(request.user.id, opts.get_content_type_id(), pk_value, repr(new_object), log.ADDITION) + msg = 'The %s "%s" was added successfully.' % (opts.verbose_name, new_object) + # Here, we distinguish between different save types by checking for + # the presence of keys in request.POST. + if request.POST.has_key("_continue"): + request.user.add_message("%s You may edit it again below." % msg) + if request.POST.has_key("_popup"): + post_url_continue += "?_popup=1" + return HttpResponseRedirect(post_url_continue % pk_value) + if request.POST.has_key("_popup"): + return HttpResponse('' % \ + (pk_value, repr(new_object).replace('"', '\\"'))) + elif request.POST.has_key("_addanother"): + request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name)) + return HttpResponseRedirect(request.path) + else: + request.user.add_message(msg) + return HttpResponseRedirect(post_url) + if request.POST.has_key("_preview"): + manipulator.do_html2python(new_data) + else: + # Add default data. + new_data = manipulator.flatten_data() + + # Override the defaults with request.GET, if it exists. + new_data.update(request.GET) + errors = {} + + # Populate the FormWrapper. + form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline=True) + + c = Context(request, { + 'title': 'Add %s' % opts.verbose_name, + 'form': form, + 'is_popup': request.REQUEST.has_key('_popup'), + }) + if object_id_override is not None: + c['object_id'] = object_id_override + + + fill_extra_context(opts, app_label, c, change=True) + + return render_to_response("admin_change_form", context_instance=c) + + + +def change_stage_new(request, app_label, module_name, object_id): + mod, opts = _get_mod_opts(app_label, module_name) + if not request.user.has_perm(app_label + '.' + opts.get_change_permission()): + raise PermissionDenied + if request.POST and request.POST.has_key("_saveasnew"): + return add_stage_new(request, app_label, module_name, form_url='../add/') + try: + manipulator = mod.ChangeManipulator(object_id) + except ObjectDoesNotExist: + raise Http404 + + inline_related_objects = opts.get_inline_related_objects() + if request.POST: + new_data = request.POST.copy() + if opts.has_field_type(meta.FileField): + new_data.update(request.FILES) + + errors = manipulator.get_validation_errors(new_data) + if not errors and not request.POST.has_key("_preview"): + for f in opts.many_to_many: + if f.rel.raw_id_admin: + new_data.setlist(f.name, new_data[f.name].split(",")) + manipulator.do_html2python(new_data) + new_object = manipulator.save(new_data) + pk_value = getattr(new_object, opts.pk.column) + + # Construct the change message. + change_message = [] + if manipulator.fields_added: + change_message.append('Added %s.' % get_text_list(manipulator.fields_added, 'and')) + if manipulator.fields_changed: + change_message.append('Changed %s.' % get_text_list(manipulator.fields_changed, 'and')) + if manipulator.fields_deleted: + change_message.append('Deleted %s.' % get_text_list(manipulator.fields_deleted, 'and')) + change_message = ' '.join(change_message) + if not change_message: + change_message = 'No fields changed.' + + log.log_action(request.user.id, opts.get_content_type_id(), pk_value, repr(new_object), log.CHANGE, change_message) + msg = 'The %s "%s" was changed successfully.' % (opts.verbose_name, new_object) + if request.POST.has_key("_continue"): + request.user.add_message("%s You may edit it again below." % msg) + if request.REQUEST.has_key('_popup'): + return HttpResponseRedirect(request.path + "?_popup=1") + else: + return HttpResponseRedirect(request.path) + elif request.POST.has_key("_saveasnew"): + request.user.add_message('The %s "%s" was added successfully. You may edit it again below.' % (opts.verbose_name, new_object)) + return HttpResponseRedirect("../%s/" % pk_value) + elif request.POST.has_key("_addanother"): + request.user.add_message("%s You may add another %s below." % (msg, opts.verbose_name)) + return HttpResponseRedirect("../add/") + else: + request.user.add_message(msg) + return HttpResponseRedirect("../") + if request.POST.has_key("_preview"): + manipulator.do_html2python(new_data) + else: + # Populate new_data with a "flattened" version of the current data. + new_data = manipulator.flatten_data() + + + # If the object has ordered objects on its admin page, get the existing + # order and flatten it into a comma-separated list of IDs. + id_order_list = [] + for rel_obj in opts.get_ordered_objects(): + id_order_list.extend(getattr(obj, 'get_%s_order' % rel_obj.object_name.lower())()) + if id_order_list: + new_data['order_'] = ','.join(map(str, id_order_list)) + errors = {} + + # Populate the FormWrapper. + form = formfields.FormWrapper(manipulator, new_data, errors, edit_inline = True) + form.original = manipulator.original_object + form.order_objects = [] + + for rel_opts, rel_field in inline_related_objects: + if rel_opts.order_with_respect_to and rel_opts.order_with_respect_to.rel and rel_opts.order_with_respect_to.rel.to == opts: + form.order_objects.extend(orig_list) + + c = Context(request, { + 'title': 'Change %s' % opts.verbose_name, + 'form': form, + 'object_id': object_id, + 'original': manipulator.original_object, + 'is_popup' : request.REQUEST.has_key('_popup') + }) + + fill_extra_context(opts, app_label, c, change=True) + + #t = template_loader.get_template_from_string(raw_template) + + return render_to_response('admin_change_form', context_instance=c); + + def _get_template(opts, app_label, add=False, change=False, show_delete=False, form_url=''): admin_field_objs = opts.admin.get_field_objs(opts) ordered_objects = opts.get_ordered_objects()[:] @@ -608,6 +912,7 @@ def _get_template(opts, app_label, add=False, change=False, show_delete=False, f for rel_obj, rel_field in opts.get_inline_related_objects(): var_name = rel_obj.object_name.lower() field_list = [f for f in rel_obj.fields + rel_obj.many_to_many if f.editable and f != rel_field] + t.append('
\n' % ((rel_field.rel.edit_inline != meta.TABULAR) and ' aligned' or '')) view_on_site = '' if change and hasattr(rel_obj, 'get_absolute_url'): @@ -680,7 +985,7 @@ def _get_template(opts, app_label, add=False, change=False, show_delete=False, f else: t.append('document.getElementById("id_%s").onchange = function() { this._changed = true; };' % field.name) for f in field.prepopulate_from: - t.append('document.getElementById("id_%s").onkeyup = function() { var e = document.getElementById("id_%s"); if (!e._changed) { e.value = URLify(%s, %s);}};' % \ + t.append('document.getElementById("id_%s").onkeyup = function() { var e = document.getElementById("id_%s"); if (e._changed) { e.value = URLify(%s, %s);}};' % \ (f, field.name, ' + " " + '.join(['document.getElementById("id_%s").value' % g for g in field.prepopulate_from]), field.maxlength)) t.append('\n') if change and ordered_objects: @@ -802,7 +1107,7 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p # Add default data. for f in opts.fields: if f.has_default(): - new_data.update(_get_flattened_data(f, f.get_default())) + new_data.update( f.flatten_data() ) # In required many-to-one fields with only one available choice, # select that one available choice. Note: We have to check that # the length of choices is *2*, not 1, because SelectFields always @@ -837,7 +1142,9 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p if f.editable and f != rel_field and not isinstance(f, meta.AutoField): for field_name in f.get_manipulator_field_names(''): full_field_name = '%s.%d.%s' % (var_name, i, field_name) - collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, ''), errors.get(full_field_name, [])) + field = manipulator[full_field_name] + data = field.extract_data(new_data) + collection[field_name] = formfields.FormFieldWrapper(field, data, errors.get(full_field_name, [])) wrapper.append(formfields.FormFieldCollection(collection)) setattr(form, rel_opts.module_name, wrapper) @@ -849,7 +1156,6 @@ def add_stage(request, app_label, module_name, show_delete=False, form_url='', p if object_id_override is not None: c['object_id'] = object_id_override raw_template = _get_template(opts, app_label, add=True, show_delete=show_delete, form_url=form_url) -# return HttpResponse(raw_template, mimetype='text/plain') t = template_loader.get_template_from_string(raw_template) return HttpResponse(t.render(c)) @@ -915,7 +1221,7 @@ def change_stage(request, app_label, module_name, object_id): new_data = {} obj = manipulator.original_object for f in opts.fields: - new_data.update(_get_flattened_data(f, getattr(obj, f.column))) + new_data.update(f.flatten_data(obj)) for f in opts.many_to_many: get_list_func = getattr(obj, 'get_%s_list' % f.rel.singular) if f.rel.raw_id_admin: @@ -927,7 +1233,7 @@ def change_stage(request, app_label, module_name, object_id): for i, rel_instance in enumerate(getattr(obj, 'get_%s_list' % opts.get_rel_object_method_name(rel_obj, rel_field))()): for f in rel_obj.fields: if f.editable and f != rel_field: - for k, v in _get_flattened_data(f, getattr(rel_instance, f.column)).items(): + for k, v in f.flatten_data(rel_instance).items(): new_data['%s.%d.%s' % (var_name, i, k)] = v for f in rel_obj.many_to_many: new_data['%s.%d.%s' % (var_name, i, f.column)] = [j.id for j in getattr(rel_instance, 'get_%s_list' % f.rel.singular)()] @@ -960,7 +1266,9 @@ def change_stage(request, app_label, module_name, object_id): if f.editable and f != rel_field: for field_name in f.get_manipulator_field_names(''): full_field_name = '%s.%d.%s' % (var_name, i, field_name) - collection[field_name] = formfields.FormFieldWrapper(manipulator[full_field_name], new_data.get(full_field_name, f.get_default()), errors.get(full_field_name, [])) + field = manipulator[full_field_name] + data = field.extract_data(new_data) + collection[field_name] = formfields.FormFieldWrapper(field, data, errors.get(full_field_name, [])) wrapper.append(formfields.FormFieldCollection(collection)) setattr(form, rel_opts.module_name, wrapper) if rel_opts.order_with_respect_to and rel_opts.order_with_respect_to.rel and rel_opts.order_with_respect_to.rel.to == opts: diff --git a/tests/runtests.py b/tests/runtests.py index 33ef7e0455..756904f56e 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -100,7 +100,8 @@ class TestRunner: self.output(1, "Creating test database") try: cursor.execute("CREATE DATABASE %s" % TEST_DATABASE_NAME) - except: + except Exception, e: + self.output(0, "There was an error creating the test database:%s " % str(e)) confirm = raw_input("The test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_DATABASE_NAME) if confirm == 'yes': cursor.execute("DROP DATABASE %s" % TEST_DATABASE_NAME)