diff --git a/django/contrib/comments/models/comments.py b/django/contrib/comments/models/comments.py index 61cb6664fc..33a44c8494 100644 --- a/django/contrib/comments/models/comments.py +++ b/django/contrib/comments/models/comments.py @@ -2,59 +2,56 @@ from django.core import meta from django.models import auth, core class Comment(meta.Model): - db_table = 'comments' - fields = ( - meta.ForeignKey(auth.User, raw_id_admin=True), - meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type'), - meta.IntegerField('object_id', 'object ID'), - meta.CharField('headline', 'headline', maxlength=255, blank=True), - meta.TextField('comment', 'comment', maxlength=3000), - meta.PositiveSmallIntegerField('rating1', 'rating #1', blank=True, null=True), - meta.PositiveSmallIntegerField('rating2', 'rating #2', blank=True, null=True), - meta.PositiveSmallIntegerField('rating3', 'rating #3', blank=True, null=True), - meta.PositiveSmallIntegerField('rating4', 'rating #4', blank=True, null=True), - meta.PositiveSmallIntegerField('rating5', 'rating #5', blank=True, null=True), - meta.PositiveSmallIntegerField('rating6', 'rating #6', blank=True, null=True), - meta.PositiveSmallIntegerField('rating7', 'rating #7', blank=True, null=True), - meta.PositiveSmallIntegerField('rating8', 'rating #8', blank=True, null=True), - # This field designates whether to use this row's ratings in - # aggregate functions (summaries). We need this because people are - # allowed to post multiple review on the same thing, but the system - # will only use the latest one (with valid_rating=True) in tallying - # the reviews. - meta.BooleanField('valid_rating', 'is valid rating'), - meta.DateTimeField('submit_date', 'date/time submitted', auto_now_add=True), - meta.BooleanField('is_public', 'is public'), - meta.IPAddressField('ip_address', 'IP address', blank=True, null=True), - meta.BooleanField('is_removed', 'is removed', - help_text='Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.'), - meta.ForeignKey(core.Site), - ) - module_constants = { - # min. and max. allowed dimensions for photo resizing (in pixels) - 'MIN_PHOTO_DIMENSION': 5, - 'MAX_PHOTO_DIMENSION': 1000, + user = meta.ForeignKey(auth.User, raw_id_admin=True) + content_type = meta.ForeignKey(core.ContentType) + object_id = meta.IntegerField('object ID') + headline = meta.CharField(maxlength=255, blank=True) + comment = meta.TextField(maxlength=3000) + rating1 = meta.PositiveSmallIntegerField('rating #1', blank=True, null=True) + rating2 = meta.PositiveSmallIntegerField('rating #2', blank=True, null=True) + rating3 = meta.PositiveSmallIntegerField('rating #3', blank=True, null=True) + rating4 = meta.PositiveSmallIntegerField('rating #4', blank=True, null=True) + rating5 = meta.PositiveSmallIntegerField('rating #5', blank=True, null=True) + rating6 = meta.PositiveSmallIntegerField('rating #6', blank=True, null=True) + rating7 = meta.PositiveSmallIntegerField('rating #7', blank=True, null=True) + rating8 = meta.PositiveSmallIntegerField('rating #8', blank=True, null=True) + # This field designates whether to use this row's ratings in aggregate + # functions (summaries). We need this because people are allowed to post + # multiple reviews on the same thing, but the system will only use the + # latest one (with valid_rating=True) in tallying the reviews. + valid_rating = meta.BooleanField('is valid rating') + submit_date = meta.DateTimeField('date/time submitted', auto_now_add=True) + is_public = meta.BooleanField() + ip_address = meta.IPAddressField('IP address', blank=True, null=True) + is_removed = meta.BooleanField(help_text='Check this box if the comment is inappropriate. A "This comment has been removed" message will be displayed instead.') + site = meta.ForeignKey(core.Site) + class META: + db_table = 'comments' + module_constants = { + # min. and max. allowed dimensions for photo resizing (in pixels) + 'MIN_PHOTO_DIMENSION': 5, + 'MAX_PHOTO_DIMENSION': 1000, - # option codes for comment-form hidden fields - 'PHOTOS_REQUIRED': 'pr', - 'PHOTOS_OPTIONAL': 'pa', - 'RATINGS_REQUIRED': 'rr', - 'RATINGS_OPTIONAL': 'ra', - 'IS_PUBLIC': 'ip', - } - ordering = ('-submit_date',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('content_type_id', 'object_id', 'site_id')}), - ('Content', {'fields': ('user_id', 'headline', 'comment')}), - ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), - ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), - ), - list_display = ('user_id', 'submit_date', 'content_type_id', 'get_content_object'), - list_filter = ('submit_date',), - date_hierarchy = 'submit_date', - search_fields = ('comment', 'user__username'), - ) + # option codes for comment-form hidden fields + 'PHOTOS_REQUIRED': 'pr', + 'PHOTOS_OPTIONAL': 'pa', + 'RATINGS_REQUIRED': 'rr', + 'RATINGS_OPTIONAL': 'ra', + 'IS_PUBLIC': 'ip', + } + ordering = ('-submit_date',) + admin = meta.Admin( + fields = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('user', 'headline', 'comment')}), + ('Ratings', {'fields': ('rating1', 'rating2', 'rating3', 'rating4', 'rating5', 'rating6', 'rating7', 'rating8', 'valid_rating')}), + ('Meta', {'fields': ('is_public', 'is_removed', 'ip_address')}), + ), + list_display = ('user', 'submit_date', 'content_type', 'get_content_object'), + list_filter = ('submit_date',), + date_hierarchy = 'submit_date', + search_fields = ('comment', 'user__username'), + ) def __repr__(self): return "%s: %s..." % (self.get_user().username, self.comment[:100]) @@ -156,32 +153,31 @@ class Comment(meta.Model): return False class FreeComment(meta.Model): - "A FreeComment is a comment by a non-registered user" - db_table = 'comments_free' - fields = ( - meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type'), - meta.IntegerField('object_id', 'object ID'), - meta.TextField('comment', 'comment', maxlength=3000), - meta.CharField('person_name', "person's name", maxlength=50), - meta.DateTimeField('submit_date', 'date/time submitted', auto_now_add=True), - meta.BooleanField('is_public', 'is public'), - meta.IPAddressField('ip_address', 'IP address'), - # TODO: Change this to is_removed, like Comment - meta.BooleanField('approved', 'approved by staff'), - meta.ForeignKey(core.Site), - ) - ordering = ('-submit_date',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('content_type_id', 'object_id', 'site_id')}), - ('Content', {'fields': ('person_name', 'comment')}), - ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}), - ), - list_display = ('person_name', 'submit_date', 'content_type_id', 'get_content_object'), - list_filter = ('submit_date',), - date_hierarchy = 'submit_date', - search_fields = ('comment', 'person_name'), - ) + # A FreeComment is a comment by a non-registered user. + content_type = meta.ForeignKey(core.ContentType) + object_id = meta.IntegerField('object ID') + comment = meta.TextField(maxlength=3000) + person_name = meta.CharField("person's name", maxlength=50) + submit_date = meta.DateTimeField('date/time submitted', auto_now_add=True) + is_public = meta.BooleanField() + ip_address = meta.IPAddressField() + # TODO: Change this to is_removed, like Comment + approved = meta.BooleanField('approved by staff') + site = meta.ForeignKey(core.Site) + class META: + db_table = 'comments_free' + ordering = ('-submit_date',) + admin = meta.Admin( + fields = ( + (None, {'fields': ('content_type', 'object_id', 'site')}), + ('Content', {'fields': ('person_name', 'comment')}), + ('Meta', {'fields': ('submit_date', 'is_public', 'ip_address', 'approved')}), + ), + list_display = ('person_name', 'submit_date', 'content_type', 'get_content_object'), + list_filter = ('submit_date',), + date_hierarchy = 'submit_date', + search_fields = ('comment', 'person_name'), + ) def __repr__(self): return "%s: %s..." % (self.person_name, self.comment[:100]) @@ -203,26 +199,25 @@ class FreeComment(meta.Model): get_content_object.short_description = 'Content object' class KarmaScore(meta.Model): - module_name = 'karma' - fields = ( - meta.ForeignKey(auth.User), - meta.ForeignKey(Comment), - meta.SmallIntegerField('score', 'score', db_index=True), - meta.DateTimeField('scored_date', 'date scored', auto_now=True), - ) - unique_together = (('user_id', 'comment_id'),) - module_constants = { - # what users get if they don't have any karma - 'DEFAULT_KARMA': 5, - 'KARMA_NEEDED_BEFORE_DISPLAYED': 3, - } + user = meta.ForeignKey(auth.User) + comment = meta.ForeignKey(Comment) + score = meta.SmallIntegerField(db_index=True) + scored_date = meta.DateTimeField(auto_now=True) + class META: + module_name = 'karma' + unique_together = (('user', 'comment'),) + module_constants = { + # what users get if they don't have any karma + 'DEFAULT_KARMA': 5, + 'KARMA_NEEDED_BEFORE_DISPLAYED': 3, + } def __repr__(self): return "%d rating by %s" % (self.score, self.get_user()) def _module_vote(user_id, comment_id, score): try: - karma = get_object(comment_id__exact=comment_id, user_id__exact=user_id) + karma = get_object(comment__id__exact=comment_id, user__id__exact=user_id) except KarmaScoreDoesNotExist: karma = KarmaScore(None, user_id, comment_id, score, datetime.datetime.now()) karma.save() @@ -241,13 +236,12 @@ class KarmaScore(meta.Model): return int(round((4.5 * score) + 5.5)) class UserFlag(meta.Model): - db_table = 'comments_user_flags' - fields = ( - meta.ForeignKey(auth.User), - meta.ForeignKey(Comment), - meta.DateTimeField('flag_date', 'date flagged', auto_now_add=True), - ) - unique_together = (('user_id', 'comment_id'),) + user = meta.ForeignKey(auth.User) + comment = meta.ForeignKey(Comment) + flag_date = meta.DateTimeField(auto_now_add=True) + class META: + db_table = 'comments_user_flags' + unique_together = (('user', 'comment'),) def __repr__(self): return "Flag by %r" % self.get_user() @@ -261,7 +255,7 @@ class UserFlag(meta.Model): if int(comment.user_id) == int(user.id): return # A user can't flag his own comment. Fail silently. try: - f = get_object(user_id__exact=user.id, comment_id__exact=comment.id) + f = get_object(user__id__exact=user.id, comment__id__exact=comment.id) except UserFlagDoesNotExist: from django.core.mail import mail_managers f = UserFlag(None, user.id, comment.id, None) @@ -270,13 +264,12 @@ class UserFlag(meta.Model): f.save() class ModeratorDeletion(meta.Model): - db_table = 'comments_moderator_deletions' - fields = ( - meta.ForeignKey(auth.User, verbose_name='moderator'), - meta.ForeignKey(Comment), - meta.DateTimeField('deletion_date', 'date deleted', auto_now_add=True), - ) - unique_together = (('user_id', 'comment_id'),) + user = meta.ForeignKey(auth.User, verbose_name='moderator') + comment = meta.ForeignKey(Comment) + deletion_date = meta.DateTimeField(auto_now_add=True) + class META: + db_table = 'comments_moderator_deletions' + unique_together = (('user', 'comment'),) def __repr__(self): return "Moderator deletion by %r" % self.get_user() diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index f54ef1e29e..f7da28b19e 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -123,7 +123,7 @@ class CommentCountNode(template.Node): self.obj_id = template.resolve_variable(self.context_var_name, context) comment_count = get_count_function(object_id__exact=self.obj_id, content_type__package__label__exact=self.package, - content_type__python_module_name__exact=self.module, site_id__exact=SITE_ID) + content_type__python_module_name__exact=self.module, site__id__exact=SITE_ID) context[self.var_name] = comment_count return '' @@ -146,7 +146,7 @@ class CommentListNode(template.Node): 'object_id__exact': self.obj_id, 'content_type__package__label__exact': self.package, 'content_type__python_module_name__exact': self.module, - 'site_id__exact': SITE_ID, + 'site__id__exact': SITE_ID, 'select_related': True, 'order_by': (self.ordering + 'submit_date',), } diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 33972a4c52..8798c05a01 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -86,8 +86,8 @@ class PublicCommentManipulator(AuthenticationForm): def save(self, new_data): today = datetime.date.today() c = self.get_comment(new_data) - for old in comments.get_list(content_type_id__exact=new_data["content_type_id"], - object_id__exact=new_data["object_id"], user_id__exact=self.get_user_id()): + for old in comments.get_list(content_type__id__exact=new_data["content_type_id"], + object_id__exact=new_data["object_id"], user__id__exact=self.get_user_id()): # Check that this comment isn't duplicate. (Sometimes people post # comments twice by mistake.) If it is, fail silently by pretending # the comment was posted successfully. @@ -141,7 +141,7 @@ class PublicFreeCommentManipulator(formfields.Manipulator): # Check that this comment isn't duplicate. (Sometimes people post # comments twice by mistake.) If it is, fail silently by pretending # the comment was posted successfully. - for old_comment in freecomments.get_list(content_type_id__exact=new_data["content_type_id"], + for old_comment in freecomments.get_list(content_type__id__exact=new_data["content_type_id"], object_id__exact=new_data["object_id"], person_name__exact=new_data["person_name"], submit_date__year=today.year, submit_date__month=today.month, submit_date__day=today.day): diff --git a/django/contrib/comments/views/userflags.py b/django/contrib/comments/views/userflags.py index 59664f3cab..3395fe017e 100644 --- a/django/contrib/comments/views/userflags.py +++ b/django/contrib/comments/views/userflags.py @@ -16,7 +16,7 @@ def flag(request, comment_id): the flagged `comments.comments` object """ try: - comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID) + comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) except comments.CommentDoesNotExist: raise Http404 if request.POST: @@ -31,7 +31,7 @@ flag = login_required(flag) def flag_done(request, comment_id): try: - comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID) + comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) except comments.CommentDoesNotExist: raise Http404 t = template_loader.get_template('comments/flag_done') @@ -50,7 +50,7 @@ def delete(request, comment_id): the flagged `comments.comments` object """ try: - comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID) + comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) except comments.CommentDoesNotExist: raise Http404 if not comments.user_is_moderator(request.user): @@ -72,7 +72,7 @@ delete = login_required(delete) def delete_done(request, comment_id): try: - comment = comments.get_object(pk=comment_id, site_id__exact=SITE_ID) + comment = comments.get_object(pk=comment_id, site__id__exact=SITE_ID) except comments.CommentDoesNotExist: raise Http404 t = template_loader.get_template('comments/delete_done') diff --git a/django/core/management.py b/django/core/management.py index 8d7d7e2a93..9e3c84e9f1 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -67,7 +67,7 @@ def get_sql_create(mod): data_type = f.__class__.__name__ col_type = db.DATA_TYPES[data_type] if col_type is not None: - field_output = [f.name, col_type % rel_field.__dict__] + field_output = [f.column, col_type % rel_field.__dict__] field_output.append('%sNULL' % (not f.null and 'NOT ' or '')) if f.unique: field_output.append('UNIQUE') @@ -75,12 +75,12 @@ def get_sql_create(mod): field_output.append('PRIMARY KEY') if f.rel: field_output.append('REFERENCES %s (%s)' % \ - (f.rel.to.db_table, f.rel.to.get_field(f.rel.field_name).name)) + (f.rel.to.db_table, f.rel.to.get_field(f.rel.field_name).column)) table_output.append(' '.join(field_output)) if opts.order_with_respect_to: table_output.append('_order %s NULL' % db.DATA_TYPES['IntegerField']) for field_constraints in opts.unique_together: - table_output.append('UNIQUE (%s)' % ", ".join(field_constraints)) + table_output.append('UNIQUE (%s)' % ", ".join([opts.get_field(f).column for f in field_constraints])) full_statement = ['CREATE TABLE %s (' % opts.db_table] for i, line in enumerate(table_output): # Combine and add commas. @@ -94,9 +94,9 @@ def get_sql_create(mod): table_output = ['CREATE TABLE %s (' % f.get_m2m_db_table(opts)] table_output.append(' id %s NOT NULL PRIMARY KEY,' % db.DATA_TYPES['AutoField']) table_output.append(' %s_id %s NOT NULL REFERENCES %s (%s),' % \ - (opts.object_name.lower(), db.DATA_TYPES['IntegerField'], opts.db_table, opts.pk.name)) + (opts.object_name.lower(), db.DATA_TYPES['IntegerField'], opts.db_table, opts.pk.column)) table_output.append(' %s_id %s NOT NULL REFERENCES %s (%s),' % \ - (f.rel.to.object_name.lower(), db.DATA_TYPES['IntegerField'], f.rel.to.db_table, f.rel.to.pk.name)) + (f.rel.to.object_name.lower(), db.DATA_TYPES['IntegerField'], f.rel.to.db_table, f.rel.to.pk.column)) table_output.append(' UNIQUE (%s_id, %s_id)' % (opts.object_name.lower(), f.rel.to.object_name.lower())) table_output.append(');') final_output.append('\n'.join(table_output)) @@ -186,7 +186,7 @@ def get_sql_sequence_reset(mod): for klass in mod._MODELS: for f in klass._meta.fields: if isinstance(f, meta.AutoField): - output.append("SELECT setval('%s_%s_seq', (SELECT max(%s) FROM %s));" % (klass._meta.db_table, f.name, f.name, klass._meta.db_table)) + output.append("SELECT setval('%s_%s_seq', (SELECT max(%s) FROM %s));" % (klass._meta.db_table, f.column, f.column, klass._meta.db_table)) return output get_sql_sequence_reset.help_doc = "Prints the SQL statements for resetting PostgreSQL sequences for the given app(s)." get_sql_sequence_reset.args = APP_ARGS @@ -199,7 +199,7 @@ def get_sql_indexes(mod): if f.db_index: unique = f.unique and "UNIQUE " or "" output.append("CREATE %sINDEX %s_%s ON %s (%s);" % \ - (unique, klass._meta.db_table, f.name, klass._meta.db_table, f.name)) + (unique, klass._meta.db_table, f.column, klass._meta.db_table, f.column)) return output get_sql_indexes.help_doc = "Prints the CREATE INDEX SQL statements for the given app(s)." get_sql_indexes.args = APP_ARGS @@ -490,7 +490,7 @@ class ModelErrorCollection: def add(self, opts, error): self.errors.append((opts, error)) - self.outfile.write("%s.%s: %s\n" % (opts.module_name, opts.object_name, error)) + self.outfile.write("%s.%s: %s\n" % (opts.app_label, opts.module_name, error)) def validate(): "Validates all installed models." @@ -524,6 +524,8 @@ def validate(): if field_name == '?': continue if field_name.startswith('-'): field_name = field_name[1:] + if opts.order_with_respect_to and field_name == '_order': + continue try: opts.get_field(field_name, many_to_many=False) except meta.FieldDoesNotExist: diff --git a/django/core/meta/__init__.py b/django/core/meta/__init__.py index 5846fdcc5c..6f72090e84 100644 --- a/django/core/meta/__init__.py +++ b/django/core/meta/__init__.py @@ -50,15 +50,21 @@ def handle_legacy_orderlist(order_list): warnings.warn("%r ordering syntax is deprecated. Use %r instead." % (order_list, new_order_list), DeprecationWarning) return new_order_list -def orderlist2sql(order_list, prefix=''): +def orderfield2column(f, opts): + try: + return opts.get_field(f, False).column + except FieldDoesNotExist: + return f + +def orderlist2sql(order_list, opts, prefix=''): output = [] for f in handle_legacy_orderlist(order_list): if f.startswith('-'): - output.append('%s%s DESC' % (prefix, f[1:])) + output.append('%s%s DESC' % (prefix, orderfield2column(f[1:], opts))) elif f == '?': output.append('RANDOM()') else: - output.append('%s%s ASC' % (prefix, f)) + output.append('%s%s ASC' % (prefix, orderfield2column(f, opts))) return ', '.join(output) def get_module(app_label, module_name): @@ -206,7 +212,7 @@ class Options: # If a primary_key field hasn't been specified, add an # auto-incrementing primary-key ID field automatically. if self.pk is None: - self.fields.insert(0, AutoField('id', 'ID', primary_key=True)) + self.fields.insert(0, AutoField(name='id', verbose_name='ID', primary_key=True)) self.pk = self.fields[0] # Cache whether this has an AutoField. self.has_auto_field = False @@ -249,7 +255,7 @@ class Options: "Returns the full 'ORDER BY' clause for this object, according to self.ordering." if not self.ordering: return '' pre = table_prefix and (table_prefix + '.') or '' - return 'ORDER BY ' + orderlist2sql(self.ordering, pre) + return 'ORDER BY ' + orderlist2sql(self.ordering, self, pre) def get_add_permission(self): return 'add_%s' % self.object_name.lower() @@ -298,7 +304,7 @@ class Options: # subsequently loaded object with related links will override this # relationship we're adding. link_field = copy.copy(core.RelatedLink._meta.get_field('object_id')) - link_field.rel = ManyToOne(self.get_model_module().Klass, 'related_links', 'id', + link_field.rel = ManyToOne(self.get_model_module().Klass, 'id', num_in_admin=3, min_num_in_admin=3, edit_inline=TABULAR, lookup_overrides={ 'content_type__package__label__exact': self.app_label, @@ -386,34 +392,48 @@ class ModelBase(type): if not bases: return type.__new__(cls, name, bases, attrs) - # If this model is a subclass of another Model, create an Options + try: + meta_attrs = attrs.pop('META').__dict__ + del meta_attrs['__module__'] + del meta_attrs['__doc__'] + except KeyError: + meta_attrs = {} + + # Gather all attributes that are Field instances. + fields = [] + for obj_name, obj in attrs.items(): + if isinstance(obj, Field): + obj.set_name(obj_name) + fields.append(obj) + del attrs[obj_name] + + # Sort the fields in the order that they were created. The + # "creation_counter" is needed because metaclasses don't preserve the + # attribute order. + fields.sort(lambda x, y: x.creation_counter - y.creation_counter) + + # If this model is a subclass of another model, create an Options # object by first copying the base class's _meta and then updating it # with the overrides from this class. replaces_module = None if bases[0] != Model: - if not attrs.has_key('fields'): - attrs['fields'] = list(bases[0]._meta._orig_init_args['fields'][:]) - if attrs.has_key('ignore_fields'): - ignore_fields = attrs.pop('ignore_fields') - new_fields = [] - for i, f in enumerate(attrs['fields']): - if f.name not in ignore_fields: - new_fields.append(f) - attrs['fields'] = new_fields - if attrs.has_key('add_fields'): - attrs['fields'].extend(attrs.pop('add_fields')) - if attrs.has_key('replaces_module'): + field_names = [f.name for f in fields] + remove_fields = meta_attrs.pop('remove_fields', []) + for f in bases[0]._meta._orig_init_args['fields']: + if f.name not in field_names and f.name not in remove_fields: + fields.insert(0, f) + if meta_attrs.has_key('replaces_module'): # Set the replaces_module variable for now. We can't actually # do anything with it yet, because the module hasn't yet been # created. - replaces_module = attrs.pop('replaces_module').split('.') + replaces_module = meta_attrs.pop('replaces_module').split('.') # Pass any Options overrides to the base's Options instance, and # simultaneously remove them from attrs. When this is done, attrs # will be a dictionary of custom methods, plus __module__. - meta_overrides = {} - for k, v in attrs.items(): + meta_overrides = {'fields': fields} + for k, v in meta_attrs.items(): if not callable(v) and k != '__module__': - meta_overrides[k] = attrs.pop(k) + meta_overrides[k] = meta_attrs.pop(k) opts = bases[0]._meta.copy(**meta_overrides) opts.object_name = name del meta_overrides @@ -422,28 +442,31 @@ class ModelBase(type): # If the module_name wasn't given, use the class name # in lowercase, plus a trailing "s" -- a poor-man's # pluralization. - module_name = attrs.pop('module_name', name.lower() + 's'), + module_name = meta_attrs.pop('module_name', name.lower() + 's'), # If the verbose_name wasn't given, use the class name, # converted from InitialCaps to "lowercase with spaces". - verbose_name = attrs.pop('verbose_name', + verbose_name = meta_attrs.pop('verbose_name', re.sub('([A-Z])', ' \\1', name).lower().strip()), - verbose_name_plural = attrs.pop('verbose_name_plural', ''), - db_table = attrs.pop('db_table', ''), - fields = attrs.pop('fields'), - ordering = attrs.pop('ordering', None), - unique_together = attrs.pop('unique_together', None), - admin = attrs.pop('admin', None), - has_related_links = attrs.pop('has_related_links', False), - where_constraints = attrs.pop('where_constraints', None), + verbose_name_plural = meta_attrs.pop('verbose_name_plural', ''), + db_table = meta_attrs.pop('db_table', ''), + fields = fields, + ordering = meta_attrs.pop('ordering', None), + unique_together = meta_attrs.pop('unique_together', None), + admin = meta_attrs.pop('admin', None), + has_related_links = meta_attrs.pop('has_related_links', False), + where_constraints = meta_attrs.pop('where_constraints', None), object_name = name, - app_label = attrs.pop('app_label', None), - exceptions = attrs.pop('exceptions', None), - permissions = attrs.pop('permissions', None), - get_latest_by = attrs.pop('get_latest_by', None), - order_with_respect_to = attrs.pop('order_with_respect_to', None), - module_constants = attrs.pop('module_constants', None), + app_label = meta_attrs.pop('app_label', None), + exceptions = meta_attrs.pop('exceptions', None), + permissions = meta_attrs.pop('permissions', None), + get_latest_by = meta_attrs.pop('get_latest_by', None), + order_with_respect_to = meta_attrs.pop('order_with_respect_to', None), + module_constants = meta_attrs.pop('module_constants', None), ) + if meta_attrs != {}: + raise TypeError, "'class META' got invalid attribute(s): %s" % ','.join(meta_attrs.keys()) + # Dynamically create the module that will contain this class and its # associated helper functions. if replaces_module is not None: @@ -511,7 +534,7 @@ class ModelBase(type): # RECURSIVE_RELATIONSHIP_CONSTANT, create that relationship formally. if f.rel and f.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT: f.rel.to = opts - f.name = f.name or ((f.rel.name or f.rel.to.object_name.lower()) + '_' + f.rel.to.pk.name) + f.name = f.name or (f.rel.to.object_name.lower() + '_' + f.rel.to.pk.name) f.verbose_name = f.verbose_name or f.rel.to.verbose_name f.rel.field_name = f.rel.field_name or f.rel.to.pk.name # Add "get_thingie" methods for many-to-one related objects. @@ -519,14 +542,14 @@ class ModelBase(type): if isinstance(f.rel, ManyToOne): func = curry(method_get_many_to_one, f) func.__doc__ = "Returns the associated `%s.%s` object." % (f.rel.to.app_label, f.rel.to.module_name) - attrs['get_%s' % f.rel.name] = func + attrs['get_%s' % f.name] = func for f in opts.many_to_many: # Add "get_thingie" methods for many-to-many related objects. # EXAMPLES: Poll.get_site_list(), Story.get_byline_list() func = curry(method_get_many_to_many, f) func.__doc__ = "Returns a list of associated `%s.%s` objects." % (f.rel.to.app_label, f.rel.to.module_name) - attrs['get_%s_list' % f.rel.name] = func + attrs['get_%s_list' % f.rel.singular] = func # Add "set_thingie" methods for many-to-many related objects. # EXAMPLES: Poll.set_sites(), Story.set_bylines() func = curry(method_set_many_to_many, f) @@ -711,14 +734,36 @@ class Model: def method_init(opts, self, *args, **kwargs): if kwargs: for f in opts.fields: - setattr(self, f.name, kwargs.pop(f.name, f.get_default())) + if isinstance(f.rel, ManyToOne): + try: + # Assume object instance was passed in. + rel_obj = kwargs.pop(f.name) + except KeyError: + try: + # Object instance wasn't passed in -- must be an ID. + val = kwargs.pop(f.column) + except KeyError: + val = f.get_default() + else: + # Special case: You can pass in "None" for related objects if it's allowed. + if rel_obj is None and f.null: + val = None + else: + try: + val = getattr(rel_obj, f.rel.field_name) + except AttributeError: + raise TypeError, "Invalid value: %r should be a %s instance, not a %s" % (f.name, f.rel.to, type(rel_obj)) + setattr(self, f.column, val) + else: + val = kwargs.pop(f.name, f.get_default()) + setattr(self, f.name, val) if kwargs: raise TypeError, "'%s' is an invalid keyword argument for this function" % kwargs.keys()[0] for i, arg in enumerate(args): - setattr(self, opts.fields[i].name, arg) + setattr(self, opts.fields[i].column, arg) def method_eq(opts, self, other): - return isinstance(other, self.__class__) and getattr(self, opts.pk.name) == getattr(other, opts.pk.name) + return isinstance(other, self.__class__) and getattr(self, opts.pk.column) == getattr(other, opts.pk.column) def method_save(opts, self): # Run any pre-save hooks. @@ -728,41 +773,41 @@ def method_save(opts, self): cursor = db.db.cursor() # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = getattr(self, opts.pk.name) + pk_val = getattr(self, opts.pk.column) pk_set = bool(pk_val) record_exists = True if pk_set: # Determine whether a record with the primary key already exists. - cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % (opts.db_table, opts.pk.name), [pk_val]) + cursor.execute("SELECT 1 FROM %s WHERE %s=%%s LIMIT 1" % (opts.db_table, opts.pk.column), [pk_val]) # If it does already exist, do an UPDATE. if cursor.fetchone(): - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.name), False)) for f in non_pks] + 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.name for f in non_pks]), opts.pk.name), + ','.join(['%s=%%s' % f.column for f in non_pks]), opts.pk.column), db_values + [pk_val]) else: record_exists = False if not pk_set or not record_exists: - field_names = [f.name for f in opts.fields if not isinstance(f, AutoField)] + field_names = [f.column for f in opts.fields if not isinstance(f, AutoField)] placeholders = ['%s'] * len(field_names) - db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.name), True)) for f in opts.fields if not isinstance(f, AutoField)] + db_values = [f.get_db_prep_save(f.pre_save(getattr(self, f.column), True)) for f in opts.fields if not isinstance(f, AutoField)] if opts.order_with_respect_to: field_names.append('_order') # TODO: This assumes the database supports subqueries. placeholders.append('(SELECT COUNT(*) FROM %s WHERE %s = %%s)' % \ - (opts.db_table, opts.order_with_respect_to.name)) - db_values.append(getattr(self, opts.order_with_respect_to.name)) + (opts.db_table, opts.order_with_respect_to.column)) + db_values.append(getattr(self, opts.order_with_respect_to.column)) cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % (opts.db_table, ','.join(field_names), ','.join(placeholders)), db_values) if opts.has_auto_field: - setattr(self, opts.pk.name, db.get_last_insert_id(cursor, opts.db_table, opts.pk.name)) + setattr(self, opts.pk.column, db.get_last_insert_id(cursor, opts.db_table, opts.pk.column)) db.db.commit() # Run any post-save hooks. if hasattr(self, '_post_save'): self._post_save() def method_delete(opts, self): - assert getattr(self, opts.pk.name) is not None, "%r can't be deleted because it doesn't have an ID." + assert getattr(self, opts.pk.column) is not None, "%r can't be deleted because it doesn't have an ID." # Run any pre-delete hooks. if hasattr(self, '_pre_delete'): self._pre_delete() @@ -781,15 +826,15 @@ def method_delete(opts, self): sub_obj.delete() for rel_opts, rel_field in opts.get_all_related_many_to_many_objects(): cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (rel_field.get_m2m_db_table(rel_opts), - self._meta.object_name.lower()), [getattr(self, opts.pk.name)]) + self._meta.object_name.lower()), [getattr(self, opts.pk.column)]) for f in opts.many_to_many: cursor.execute("DELETE FROM %s WHERE %s_id=%%s" % (f.get_m2m_db_table(opts), self._meta.object_name.lower()), - [getattr(self, opts.pk.name)]) - cursor.execute("DELETE FROM %s WHERE %s=%%s" % (opts.db_table, opts.pk.name), [getattr(self, opts.pk.name)]) + [getattr(self, opts.pk.column)]) + cursor.execute("DELETE FROM %s WHERE %s=%%s" % (opts.db_table, opts.pk.column), [getattr(self, opts.pk.column)]) db.db.commit() - setattr(self, opts.pk.name, None) + setattr(self, opts.pk.column, None) for f in opts.fields: - if isinstance(f, FileField) and getattr(self, f.name): + if isinstance(f, FileField) and getattr(self, f.column): file_name = getattr(self, 'get_%s_filename' % f.name)() # If the file exists and no other object of this type references it, # delete it from the filesystem. @@ -802,26 +847,26 @@ def method_delete(opts, self): def method_get_next_in_order(opts, order_field, self): if not hasattr(self, '_next_in_order_cache'): self._next_in_order_cache = opts.get_model_module().get_object(order_by=('_order',), - where=['_order > (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name), - '%s=%%s' % order_field.name], limit=1, - params=[getattr(self, opts.pk.name), getattr(self, order_field.name)]) + where=['_order > (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.column), + '%s=%%s' % order_field.column], limit=1, + params=[getattr(self, opts.pk.column), getattr(self, order_field.name)]) return self._next_in_order_cache def method_get_previous_in_order(opts, order_field, self): if not hasattr(self, '_previous_in_order_cache'): self._previous_in_order_cache = opts.get_model_module().get_object(order_by=('-_order',), - where=['_order < (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.name), - '%s=%%s' % order_field.name], limit=1, - params=[getattr(self, opts.pk.name), getattr(self, order_field.name)]) + where=['_order < (SELECT _order FROM %s WHERE %s=%%s)' % (opts.db_table, opts.pk.column), + '%s=%%s' % order_field.column], limit=1, + params=[getattr(self, opts.pk.column), getattr(self, order_field.name)]) return self._previous_in_order_cache # RELATIONSHIP METHODS ##################### # Example: Story.get_dateline() def method_get_many_to_one(field_with_rel, self): - cache_var = field_with_rel.rel.get_cache_name() + cache_var = field_with_rel.get_cache_name() if not hasattr(self, cache_var): - val = getattr(self, field_with_rel.name) + val = getattr(self, field_with_rel.column) mod = field_with_rel.rel.to.get_model_module() if val is None: raise getattr(mod, '%sDoesNotExist' % field_with_rel.rel.to.object_name) @@ -837,11 +882,11 @@ def method_get_many_to_many(field_with_rel, self): if not hasattr(self, cache_var): mod = rel.get_model_module() sql = "SELECT %s FROM %s a, %s b WHERE a.%s = b.%s_id AND b.%s_id = %%s %s" % \ - (','.join(['a.%s' % f.name for f in rel.fields]), rel.db_table, - field_with_rel.get_m2m_db_table(self._meta), rel.pk.name, + (','.join(['a.%s' % f.column for f in rel.fields]), rel.db_table, + field_with_rel.get_m2m_db_table(self._meta), rel.pk.column, rel.object_name.lower(), self._meta.object_name.lower(), rel.get_order_sql('a')) cursor = db.db.cursor() - cursor.execute(sql, [getattr(self, self._meta.pk.name)]) + cursor.execute(sql, [getattr(self, self._meta.pk.column)]) setattr(self, cache_var, [getattr(mod, rel.object_name)(*row) for row in cursor.fetchall()]) return getattr(self, cache_var) @@ -863,7 +908,7 @@ def method_set_many_to_many(rel_field, self, id_list): rel = rel_field.rel.to m2m_table = rel_field.get_m2m_db_table(self._meta) cursor = db.db.cursor() - this_id = getattr(self, self._meta.pk.name) + this_id = getattr(self, self._meta.pk.column) if ids_to_delete: sql = "DELETE FROM %s WHERE %s_id = %%s AND %s_id IN (%s)" % (m2m_table, self._meta.object_name.lower(), rel.object_name.lower(), ','.join(map(str, ids_to_delete))) cursor.execute(sql, [this_id]) @@ -880,7 +925,7 @@ def method_set_many_to_many(rel_field, self, id_list): # Handles related-object retrieval. # Examples: Poll.get_choice(), Poll.get_choice_list(), Poll.get_choice_count() def method_get_related(method_name, rel_mod, rel_field, self, **kwargs): - kwargs['%s__exact' % rel_field.name] = getattr(self, rel_field.rel.field_name) + kwargs['%s__%s__exact' % (rel_field.name, rel_field.rel.to.pk.name)] = getattr(self, rel_field.rel.field_name) kwargs.update(rel_field.rel.lookup_overrides) return getattr(rel_mod, method_name)(**kwargs) @@ -892,7 +937,7 @@ def method_add_related(rel_obj, rel_mod, rel_field, self, *args, **kwargs): for f in rel_obj.fields: if isinstance(f, AutoField): init_kwargs[f.name] = None - init_kwargs[rel_field.name] = getattr(self, rel_field.rel.field_name) + init_kwargs[rel_field.name] = self obj = rel_mod.Klass(**init_kwargs) obj.save() return obj @@ -909,7 +954,7 @@ def method_set_related_many_to_many(rel_opts, rel_field, self, id_list): id_list = map(int, id_list) # normalize to integers rel = rel_field.rel.to m2m_table = rel_field.get_m2m_db_table(rel_opts) - this_id = getattr(self, self._meta.pk.name) + this_id = getattr(self, self._meta.pk.column) cursor = db.db.cursor() cursor.execute("DELETE FROM %s WHERE %s_id = %%s" % (m2m_table, rel.object_name.lower()), [this_id]) sql = "INSERT INTO %s (%s_id, %s_id) VALUES (%%s, %%s)" % (m2m_table, rel.object_name.lower(), rel_opts.object_name.lower()) @@ -921,7 +966,7 @@ def method_set_related_many_to_many(rel_opts, rel_field, self, id_list): def method_set_order(ordered_obj, self, id_list): cursor = db.db.cursor() # Example: "UPDATE poll_choices SET _order = %s WHERE poll_id = %s AND id = %s" - sql = "UPDATE %s SET _order = %%s WHERE %s = %%s AND %s = %%s" % (ordered_obj.db_table, ordered_obj.order_with_respect_to.name, ordered_obj.pk.name) + sql = "UPDATE %s SET _order = %%s WHERE %s = %%s AND %s = %%s" % (ordered_obj.db_table, ordered_obj.order_with_respect_to.column, ordered_obj.pk.column) rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) cursor.executemany(sql, [(i, rel_val, j) for i, j in enumerate(id_list)]) db.db.commit() @@ -929,7 +974,7 @@ def method_set_order(ordered_obj, self, id_list): def method_get_order(ordered_obj, self): cursor = db.db.cursor() # Example: "SELECT id FROM poll_choices WHERE poll_id = %s ORDER BY _order" - sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY _order" % (ordered_obj.pk.name, ordered_obj.db_table, ordered_obj.order_with_respect_to.name) + sql = "SELECT %s FROM %s WHERE %s = %%s ORDER BY _order" % (ordered_obj.pk.column, ordered_obj.db_table, ordered_obj.order_with_respect_to.column) rel_val = getattr(self, ordered_obj.order_with_respect_to.rel.field_name) cursor.execute(sql, [rel_val]) return [r[0] for r in cursor.fetchall()] @@ -937,7 +982,7 @@ def method_get_order(ordered_obj, self): # DATE-RELATED METHODS ##################### def method_get_next_or_previous(get_object_func, field, is_next, self, **kwargs): - kwargs.setdefault('where', []).append('%s %s %%s' % (field.name, (is_next and '>' or '<'))) + kwargs.setdefault('where', []).append('%s %s %%s' % (field.column, (is_next and '>' or '<'))) kwargs.setdefault('params', []).append(str(getattr(self, field.name))) kwargs['order_by'] = [(not is_next and '-' or '') + field.name] kwargs['limit'] = 1 @@ -1045,7 +1090,7 @@ def _get_cached_row(opts, row, index_start): for f in opts.fields: if f.rel and not f.null: rel_obj, index_end = _get_cached_row(f.rel.to, row, index_end) - setattr(obj, f.rel.get_cache_name(), rel_obj) + setattr(obj, f.get_cache_name(), rel_obj) return obj, index_end def function_get_iterator(opts, klass, **kwargs): @@ -1091,9 +1136,9 @@ def function_get_values_iterator(opts, klass, **kwargs): # 'fields' is a list of field names to fetch. try: - fields = kwargs.pop('fields') + fields = [opts.get_field(f).column for f in kwargs.pop('fields')] except KeyError: # Default to all fields. - fields = [f.name for f in opts.fields] + fields = [f.column for f in opts.fields] cursor = db.db.cursor() _, sql, params = function_get_sql_clause(opts, **kwargs) @@ -1124,8 +1169,8 @@ def _fill_table_cache(opts, select, tables, where, old_prefix, cache_tables_seen tables.append('%s %s' % (db_table, new_prefix)) db_table = new_prefix cache_tables_seen.append(db_table) - where.append('%s.%s = %s.%s' % (old_prefix, f.name, db_table, f.rel.field_name)) - select.extend(['%s.%s' % (db_table, f2.name) for f2 in f.rel.to.fields]) + where.append('%s.%s = %s.%s' % (old_prefix, f.column, db_table, f.rel.get_related_field().column)) + select.extend(['%s.%s' % (db_table, f2.column) for f2 in f.rel.to.fields]) _fill_table_cache(f.rel.to, select, tables, where, db_table, cache_tables_seen) def _throw_bad_kwarg_error(kwarg): @@ -1158,7 +1203,10 @@ def _parse_lookup(kwarg_items, opts, table_count=0): lookup_list = kwarg.split(LOOKUP_SEPARATOR) # pk="value" is shorthand for (primary key)__exact="value" if lookup_list[-1] == 'pk': - lookup_list = lookup_list[:-1] + [opts.pk.name, 'exact'] + if opts.pk.rel: + lookup_list = lookup_list[:-1] + [opts.pk.name, opts.pk.rel.field_name, 'exact'] + else: + lookup_list = lookup_list[:-1] + [opts.pk.name, 'exact'] if len(lookup_list) == 1: _throw_bad_kwarg_error(kwarg) lookup_type = lookup_list.pop() @@ -1184,7 +1232,7 @@ def _parse_lookup(kwarg_items, opts, table_count=0): rel_table_alias = 't%s' % table_count table_count += 1 tables.append('%s %s' % (f.get_m2m_db_table(current_opts), rel_table_alias)) - join_where.append('%s.%s = %s.%s_id' % (current_table_alias, current_opts.pk.name, + join_where.append('%s.%s = %s.%s_id' % (current_table_alias, current_opts.pk.column, rel_table_alias, current_opts.object_name.lower())) # Optimization: In the case of primary-key lookups, we # don't have to do an extra join. @@ -1198,32 +1246,39 @@ def _parse_lookup(kwarg_items, opts, table_count=0): new_table_alias = 't%s' % table_count tables.append('%s %s' % (f.rel.to.db_table, new_table_alias)) join_where.append('%s.%s_id = %s.%s' % (rel_table_alias, f.rel.to.object_name.lower(), - new_table_alias, f.rel.to.pk.name)) + new_table_alias, f.rel.to.pk.column)) current_table_alias = new_table_alias param_required = True current_opts = f.rel.to raise StopIteration for f in current_opts.fields: # Try many-to-one relationships... - if f.rel and f.rel.name == current: + if f.rel and f.name == current: # Optimization: In the case of primary-key lookups, we # don't have to do an extra join. if lookup_list and lookup_list[0] == f.rel.to.pk.name and lookup_type == 'exact': - where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.name, kwarg_value)) + where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) lookup_list.pop() param_required = False + # 'isnull' lookups in many-to-one relationships are a special case, + # because we don't want to do a join. We just want to find out + # whether the foreign key field is NULL. + elif lookup_type == 'isnull' and not lookup_list: + where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) + params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) else: new_table_alias = 't%s' % table_count tables.append('%s %s' % (f.rel.to.db_table, new_table_alias)) - join_where.append('%s.%s = %s.%s' % (current_table_alias, f.name, new_table_alias, f.rel.to.pk.name)) + join_where.append('%s.%s = %s.%s' % (current_table_alias, f.column, \ + new_table_alias, f.rel.to.pk.column)) current_table_alias = new_table_alias param_required = True current_opts = f.rel.to raise StopIteration # Try direct field-name lookups... if f.name == current: - where.append(_get_where_clause(lookup_type, current_table_alias+'.', current, kwarg_value)) + where.append(_get_where_clause(lookup_type, current_table_alias+'.', f.column, kwarg_value)) params.extend(f.get_db_prep_lookup(lookup_type, kwarg_value)) param_required = False raise StopIteration @@ -1235,7 +1290,7 @@ def _parse_lookup(kwarg_items, opts, table_count=0): return tables, join_where, where, params, table_count def function_get_sql_clause(opts, **kwargs): - select = ["%s.%s" % (opts.db_table, f.name) for f in opts.fields] + select = ["%s.%s" % (opts.db_table, f.column) for f in opts.fields] tables = [opts.db_table] + (kwargs.get('tables') and kwargs['tables'][:] or []) where = kwargs.get('where') and kwargs['where'][:] or [] params = kwargs.get('params') and kwargs['params'][:] or [] @@ -1270,9 +1325,9 @@ def function_get_sql_clause(opts, **kwargs): else: table_prefix = '' if f.startswith('-'): - order_by.append('%s%s DESC' % (table_prefix, f[1:])) + order_by.append('%s%s DESC' % (table_prefix, orderfield2column(f[1:], opts))) else: - order_by.append('%s%s ASC' % (table_prefix, f)) + order_by.append('%s%s ASC' % (table_prefix, orderfield2column(f, opts))) order_by = ", ".join(order_by) # LIMIT and OFFSET clauses @@ -1307,9 +1362,9 @@ def function_get_date_list(opts, field, *args, **kwargs): assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'" kwargs['order_by'] = [] # Clear this because it'll mess things up otherwise. if field.null: - kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % (opts.db_table, field.name)) + kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % (opts.db_table, field.column)) select, sql, params = function_get_sql_clause(opts, **kwargs) - sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.name)), sql) + sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1' % (db.get_date_trunc_sql(kind, '%s.%s' % (opts.db_table, field.column)), sql) cursor = db.db.cursor() cursor.execute(sql, params) # We have to manually run typecast_timestamp(str()) on the results, because @@ -1359,8 +1414,8 @@ def manipulator_init(opts, add, change, self, obj_key=None): lookup_kwargs = opts.one_to_one_field.rel.limit_choices_to lookup_kwargs['%s__exact' % opts.one_to_one_field.rel.field_name] = obj_key _ = opts.one_to_one_field.rel.to.get_model_module().get_object(**lookup_kwargs) - params = dict([(f.name, f.get_default()) for f in opts.fields]) - params[opts.pk.name] = obj_key + params = dict([(f.column, f.get_default()) for f in opts.fields]) + params[opts.pk.column] = obj_key self.original_object = opts.get_model_module().Klass(**params) else: raise @@ -1396,9 +1451,9 @@ def manipulator_save(opts, klass, add, change, self, new_data): # Fields with auto_now_add are another special case; they should keep # their original value in the change stage. if change and getattr(f, 'auto_now_add', False): - params[f.name] = getattr(self.original_object, f.name) + params[f.column] = getattr(self.original_object, f.name) else: - params[f.name] = f.get_manipulator_new_data(new_data) + params[f.column] = f.get_manipulator_new_data(new_data) if change: params[opts.pk.name] = self.obj_key @@ -1416,7 +1471,7 @@ def manipulator_save(opts, klass, add, change, self, new_data): if change: self.fields_added, self.fields_changed, self.fields_deleted = [], [], [] for f in opts.fields: - if not f.primary_key and str(getattr(self.original_object, f.name)) != str(getattr(new_object, f.name)): + if not f.primary_key and str(getattr(self.original_object, f.column)) != str(getattr(new_object, f.column)): self.fields_changed.append(f.verbose_name) # Save many-to-many objects. Example: Poll.set_sites() @@ -1467,15 +1522,15 @@ def manipulator_save(opts, klass, add, change, self, new_data): # case, because they'll be dealt with later. if change and (isinstance(f, FileField) or not f.editable): if rel_new_data.get(rel_opts.pk.name, False) and rel_new_data[rel_opts.pk.name][0]: - params[f.name] = getattr(old_rel_obj, f.name) + params[f.column] = getattr(old_rel_obj, f.column) else: - params[f.name] = f.get_default() + params[f.column] = f.get_default() elif f == rel_field: - params[f.name] = getattr(new_object, rel_field.rel.field_name) + params[f.column] = getattr(new_object, rel_field.rel.field_name) elif add and isinstance(f, AutoField): - params[f.name] = None + params[f.column] = None else: - params[f.name] = f.get_manipulator_new_data(rel_new_data, rel=True) + params[f.column] = f.get_manipulator_new_data(rel_new_data, rel=True) # Related links are a special case, because we have to # manually set the "content_type_id" field. if opts.has_related_links and rel_opts.module_name == 'relatedlinks': @@ -1501,7 +1556,7 @@ def manipulator_save(opts, klass, add, change, self, new_data): self.fields_added.append('%s "%r"' % (rel_opts.verbose_name, new_rel_obj)) else: for f in rel_opts.fields: - if not f.primary_key and f != rel_field and str(getattr(old_rel_obj, f.name)) != str(getattr(new_rel_obj, f.name)): + if not f.primary_key and f != rel_field and str(getattr(old_rel_obj, f.column)) != str(getattr(new_rel_obj, f.column)): self.fields_changed.append('%s for %s "%r"' % (f.verbose_name, rel_opts.verbose_name, new_rel_obj)) # Save many-to-many objects. @@ -1527,20 +1582,26 @@ def manipulator_save(opts, klass, add, change, self, 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] - kwargs = {'%s__iexact' % field_name_list[0]: field_data} + if isinstance(field_list[0].rel, ManyToOne): + kwargs = {'%s__%s__iexact' % (field_name_list[0], field_list[0].rel.field_name): field_data} + else: + kwargs = {'%s__iexact' % field_name_list[0]: field_data} for f in field_list[1:]: - field_val = all_data.get(f.name, None) + field_val = all_data.get(f.column, None) if field_val is None: # This will be caught by another validator, assuming the field # doesn't have blank=True. return - kwargs['%s__iexact' % f.name] = field_val + if isinstance(f.rel, ManyToOne): + kwargs['%s__pk' % f.name] = field_val + else: + kwargs['%s__iexact' % f.name] = field_val mod = opts.get_model_module() try: old_obj = mod.get_object(**kwargs) except ObjectDoesNotExist: return - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name): + if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.column) == getattr(old_obj, opts.pk.column): pass else: raise validators.ValidationError, "%s with this %s already exists for the given %s." % \ @@ -1562,7 +1623,7 @@ def manipulator_validator_unique_for_date(from_field, date_field, opts, lookup_t except ObjectDoesNotExist: return else: - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name): + if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.column) == getattr(old_obj, opts.pk.column): pass else: format_string = (lookup_type == 'date') and '%B %d, %Y' or '%B %Y' diff --git a/django/core/meta/fields.py b/django/core/meta/fields.py index 774c459279..da201ef8f3 100644 --- a/django/core/meta/fields.py +++ b/django/core/meta/fields.py @@ -40,7 +40,7 @@ def manipulator_validator_unique(f, opts, self, field_data, all_data): old_obj = opts.get_model_module().get_object(**{'%s__exact' % f.name: field_data}) except ObjectDoesNotExist: return - if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.name) == getattr(old_obj, opts.pk.name): + if hasattr(self, 'original_object') and getattr(self.original_object, opts.pk.column) == getattr(old_obj, opts.pk.column): return raise validators.ValidationError, "%s with this %s already exists." % (capfirst(opts.verbose_name), f.verbose_name) @@ -50,14 +50,17 @@ class Field(object): # database level. empty_strings_allowed = True - def __init__(self, name, verbose_name=None, primary_key=False, + # Tracks each time a Field instance is created. Used to retain order. + creation_counter = 0 + + def __init__(self, verbose_name=None, name=None, primary_key=False, maxlength=None, unique=False, blank=False, null=False, db_index=None, core=False, rel=None, default=NOT_PROVIDED, editable=True, prepopulate_from=None, unique_for_date=None, unique_for_month=None, unique_for_year=None, validator_list=None, choices=None, radio_admin=None, - help_text=''): + help_text='', db_column=None): self.name = name - self.verbose_name = verbose_name or name.replace('_', ' ') + self.verbose_name = verbose_name or (name and name.replace('_', ' ')) self.primary_key = primary_key self.maxlength, self.unique = maxlength, unique self.blank, self.null = blank, null @@ -70,6 +73,7 @@ class Field(object): self.choices = choices or [] self.radio_admin = radio_admin self.help_text = help_text + self.db_column = db_column if rel and isinstance(rel, ManyToMany): if rel.raw_id_admin: self.help_text += ' Separate multiple IDs with commas.' @@ -85,6 +89,27 @@ class Field(object): else: self.db_index = db_index + # Increase the creation counter, and save our local copy. + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 + + # Set the name of the database column. + self.column = self.get_db_column() + + def set_name(self, name): + self.name = name + self.verbose_name = self.verbose_name or name.replace('_', ' ') + self.column = self.get_db_column() + + def get_db_column(self): + if self.db_column: return self.db_column + if isinstance(self.rel, ManyToOne): + return '%s_id' % self.name + return self.name + + def get_cache_name(self): + return '_%s_cache' % self.name + def pre_save(self, value, add): "Returns field's value just before saving." return value @@ -232,7 +257,7 @@ class Field(object): if self.choices: return first_choice + list(self.choices) rel_obj = self.rel.to - return first_choice + [(getattr(x, rel_obj.pk.name), repr(x)) for x in rel_obj.get_model_module().get_list(**self.rel.limit_choices_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)] class AutoField(Field): empty_strings_allowed = False @@ -271,11 +296,11 @@ class CommaSeparatedIntegerField(CharField): class DateField(Field): empty_strings_allowed = False - def __init__(self, name, verbose_name=None, auto_now=False, auto_now_add=False, **kwargs): + def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: kwargs['editable'] = False - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_db_prep_lookup(self, lookup_type, value): if lookup_type == 'range': @@ -332,9 +357,9 @@ class EmailField(Field): return [formfields.EmailField] class FileField(Field): - def __init__(self, name, verbose_name=None, upload_to='', **kwargs): + def __init__(self, verbose_name=None, name=None, upload_to='', **kwargs): self.upload_to = upload_to - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_fields(self, opts, manipulator, change, name_prefix='', rel=False): field_list = Field.get_manipulator_fields(self, opts, manipulator, change, name_prefix, rel) @@ -397,17 +422,17 @@ class FileField(Field): class FloatField(Field): empty_strings_allowed = False - def __init__(self, name, verbose_name=None, max_digits=None, decimal_places=None, **kwargs): + def __init__(self, verbose_name=None, name=None, max_digits=None, decimal_places=None, **kwargs): self.max_digits, self.decimal_places = max_digits, decimal_places - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): return [curry(formfields.FloatField, max_digits=self.max_digits, decimal_places=self.decimal_places)] class ImageField(FileField): - def __init__(self, name, verbose_name=None, width_field=None, height_field=None, **kwargs): + def __init__(self, verbose_name=None, name=None, width_field=None, height_field=None, **kwargs): self.width_field, self.height_field = width_field, height_field - FileField.__init__(self, name, verbose_name, **kwargs) + FileField.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): return [formfields.ImageUploadField, formfields.HiddenField] @@ -479,11 +504,11 @@ class TextField(Field): class TimeField(Field): empty_strings_allowed = False - def __init__(self, name, verbose_name=None, auto_now=False, auto_now_add=False, **kwargs): + def __init__(self, verbose_name=None, name=None, auto_now=False, auto_now_add=False, **kwargs): self.auto_now, self.auto_now_add = auto_now, auto_now_add if auto_now or auto_now_add: kwargs['editable'] = False - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_db_prep_lookup(self, lookup_type, value): if lookup_type == 'range': @@ -511,10 +536,10 @@ class TimeField(Field): return [formfields.TimeField] class URLField(Field): - def __init__(self, name, verbose_name=None, verify_exists=True, **kwargs): + def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): if verify_exists: kwargs.setdefault('validator_list', []).append(validators.isExistingURL) - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): return [formfields.URLField] @@ -524,34 +549,31 @@ class USStateField(Field): return [formfields.USStateField] class XMLField(Field): - def __init__(self, name, verbose_name=None, schema_path=None, **kwargs): + def __init__(self, verbose_name=None, name=None, schema_path=None, **kwargs): self.schema_path = schema_path - Field.__init__(self, name, verbose_name, **kwargs) + Field.__init__(self, verbose_name, name, **kwargs) def get_manipulator_field_objs(self): return [curry(formfields.XMLLargeTextField, schema_path=self.schema_path)] class ForeignKey(Field): empty_strings_allowed = False - def __init__(self, to, to_field=None, rel_name=None, **kwargs): + def __init__(self, to, to_field=None, **kwargs): try: to_name = to._meta.object_name.lower() except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert to == 'self', "ForeignKey(%r) is invalid. First parameter to ForeignKey must be either a model or the string %r" % (to, RECURSIVE_RELATIONSHIP_CONSTANT) - kwargs['name'] = kwargs.get('name', '') kwargs['verbose_name'] = kwargs.get('verbose_name', '') else: to_field = to_field or to._meta.pk.name - kwargs['name'] = kwargs.get('name', to_name + '_id') kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name) - rel_name = rel_name or to_name if kwargs.has_key('edit_inline_type'): import warnings warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - kwargs['rel'] = ManyToOne(to, rel_name, 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), @@ -567,11 +589,9 @@ class ForeignKey(Field): return [formfields.IntegerField] class ManyToManyField(Field): - def __init__(self, to, rel_name=None, **kwargs): - kwargs['name'] = kwargs.get('name', to._meta.module_name) + def __init__(self, to, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', to._meta.verbose_name_plural) - rel_name = rel_name or to._meta.object_name.lower() - kwargs['rel'] = ManyToMany(to, rel_name, + kwargs['rel'] = ManyToMany(to, kwargs.pop('singular', None), num_in_admin=kwargs.pop('num_in_admin', 0), related_name=kwargs.pop('related_name', None), filter_interface=kwargs.pop('filter_interface', None), @@ -609,18 +629,16 @@ class ManyToManyField(Field): len(badkeys) == 1 and "is" or "are") class OneToOneField(IntegerField): - def __init__(self, to, to_field=None, rel_name=None, **kwargs): - kwargs['name'] = kwargs.get('name', 'id') + def __init__(self, to, to_field=None, **kwargs): kwargs['verbose_name'] = kwargs.get('verbose_name', 'ID') to_field = to_field or to._meta.pk.name - rel_name = rel_name or to._meta.object_name.lower() if kwargs.has_key('edit_inline_type'): import warnings warnings.warn("edit_inline_type is deprecated. Use edit_inline instead.") kwargs['edit_inline'] = kwargs.pop('edit_inline_type') - kwargs['rel'] = OneToOne(to, rel_name, to_field, + kwargs['rel'] = OneToOne(to, to_field, num_in_admin=kwargs.pop('num_in_admin', 0), edit_inline=kwargs.pop('edit_inline', False), related_name=kwargs.pop('related_name', None), @@ -631,7 +649,7 @@ class OneToOneField(IntegerField): IntegerField.__init__(self, **kwargs) class ManyToOne: - def __init__(self, to, name, field_name, num_in_admin=3, min_num_in_admin=None, + 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): try: @@ -639,7 +657,7 @@ class ManyToOne: except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert to == RECURSIVE_RELATIONSHIP_CONSTANT, "'to' must be either a model or the string '%s'" % RECURSIVE_RELATIONSHIP_CONSTANT self.to = to - self.name, self.field_name = name, field_name + self.field_name = field_name self.num_in_admin, self.edit_inline = num_in_admin, edit_inline 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 @@ -647,17 +665,15 @@ class ManyToOne: self.lookup_overrides = lookup_overrides or {} self.raw_id_admin = raw_id_admin - def get_cache_name(self): - return '_%s_cache' % self.name - def get_related_field(self): "Returns the Field in the 'to' object to which this relationship is tied." return self.to.get_field(self.field_name) class ManyToMany: - def __init__(self, to, name, num_in_admin=0, 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): - self.to, self.name = to._meta, name + self.to = to._meta + self.singular = singular or to._meta.object_name.lower() self.num_in_admin = num_in_admin self.related_name = related_name self.filter_interface = filter_interface @@ -667,10 +683,10 @@ class ManyToMany: assert not (self.raw_id_admin and self.filter_interface), "ManyToMany relationships may not use both raw_id_admin and filter_interface" class OneToOne(ManyToOne): - def __init__(self, to, name, field_name, num_in_admin=0, 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, raw_id_admin=False): - self.to, self.name, self.field_name = to._meta, name, field_name + self.to, self.field_name = to._meta, field_name self.num_in_admin, self.edit_inline = num_in_admin, edit_inline self.related_name = related_name self.limit_choices_to = limit_choices_to or {} diff --git a/django/models/auth.py b/django/models/auth.py index 9ab403af34..430cad192a 100644 --- a/django/models/auth.py +++ b/django/models/auth.py @@ -2,65 +2,60 @@ from django.core import meta, validators from django.models import core class Permission(meta.Model): - fields = ( - meta.CharField('name', maxlength=50), - meta.ForeignKey(core.Package, name='package'), - meta.CharField('codename', maxlength=100), - ) - unique_together = (('package', 'codename'),) - ordering = ('package', 'codename') + name = meta.CharField(maxlength=50) + package = meta.ForeignKey(core.Package, db_column='package') + codename = meta.CharField(maxlength=100) + class META: + unique_together = (('package', 'codename'),) + ordering = ('package', 'codename') def __repr__(self): return "%s | %s" % (self.package, self.name) class Group(meta.Model): - fields = ( - meta.CharField('name', maxlength=80, unique=True), - meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL), - ) - ordering = ('name',) - admin = meta.Admin( - search_fields = ('name',), - ) + name = meta.CharField(maxlength=80, unique=True) + permissions = meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL) + class META: + ordering = ('name',) + admin = meta.Admin( + search_fields = ('name',), + ) def __repr__(self): return self.name class User(meta.Model): - fields = ( - meta.CharField('username', maxlength=30, unique=True, - validator_list=[validators.isAlphaNumeric]), - meta.CharField('first_name', maxlength=30, blank=True), - meta.CharField('last_name', maxlength=30, blank=True), - meta.EmailField('email', 'e-mail address', blank=True), - meta.CharField('password_md5', 'password', maxlength=32, help_text="Use an MD5 hash -- not the raw password."), - meta.BooleanField('is_staff', 'staff status', - help_text="Designates whether the user can log into this admin site."), - meta.BooleanField('is_active', 'active', default=True), - meta.BooleanField('is_superuser', 'superuser status'), - meta.DateTimeField('last_login', default=meta.LazyDate()), - meta.DateTimeField('date_joined', default=meta.LazyDate()), - meta.ManyToManyField(Group, blank=True, - help_text="In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in."), - meta.ManyToManyField(Permission, name='user_permissions', blank=True, filter_interface=meta.HORIZONTAL), - ) - module_constants = { - 'SESSION_KEY': '_auth_user_id', - } - ordering = ('username',) - exceptions = ('SiteProfileNotAvailable',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('username', 'password_md5')}), - ('Personal info', {'fields': ('first_name', 'last_name', 'email')}), - ('Permissions', {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), - ('Important dates', {'fields': ('last_login', 'date_joined')}), - ('Groups', {'fields': ('groups',)}), - ), - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff'), - list_filter = ('is_staff', 'is_superuser'), - search_fields = ('username', 'first_name', 'last_name', 'email'), - ) + username = meta.CharField(maxlength=30, unique=True, validator_list=[validators.isAlphaNumeric]) + first_name = meta.CharField(maxlength=30, blank=True) + last_name = meta.CharField(maxlength=30, blank=True) + email = meta.EmailField('e-mail address', blank=True) + password_md5 = meta.CharField('password', maxlength=32, help_text="Use an MD5 hash -- not the raw password.") + is_staff = meta.BooleanField('staff status', help_text="Designates whether the user can log into this admin site.") + is_active = meta.BooleanField('active', default=True) + is_superuser = meta.BooleanField('superuser status') + last_login = meta.DateTimeField(default=meta.LazyDate()) + date_joined = meta.DateTimeField(default=meta.LazyDate()) + groups = meta.ManyToManyField(Group, blank=True, + help_text="In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in.") + user_permissions = meta.ManyToManyField(Permission, blank=True, filter_interface=meta.HORIZONTAL) + class META: + module_constants = { + 'SESSION_KEY': '_auth_user_id', + } + ordering = ('username',) + exceptions = ('SiteProfileNotAvailable',) + admin = meta.Admin( + fields = ( + (None, {'fields': ('username', 'password_md5')}), + ('Personal info', {'fields': ('first_name', 'last_name', 'email')}), + ('Permissions', {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}), + ('Important dates', {'fields': ('last_login', 'date_joined')}), + ('Groups', {'fields': ('groups',)}), + ), + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff'), + list_filter = ('is_staff', 'is_superuser'), + search_fields = ('username', 'first_name', 'last_name', 'email'), + ) def __repr__(self): return self.username @@ -154,7 +149,7 @@ class User(meta.Model): except ImportError: try: module = __import__('django.models.%s' % AUTH_PROFILE_MODULE, [], [], ['']) - self._profile_cache = module.get_object(user_id__exact=self.id) + self._profile_cache = module.get_object(user__id__exact=self.id) except ImportError: raise SiteProfileNotAvailable return self._profile_cache @@ -176,33 +171,30 @@ class User(meta.Model): return ''.join([choice(allowed_chars) for i in range(length)]) class Message(meta.Model): - fields = ( - meta.ForeignKey(User), - meta.TextField('message'), - ) + user = meta.ForeignKey(User) + message = meta.TextField() def __repr__(self): return self.message class LogEntry(meta.Model): - module_name = 'log' - verbose_name_plural = 'log entries' - db_table = 'auth_admin_log' - fields = ( - meta.DateTimeField('action_time', auto_now=True), - meta.ForeignKey(User), - meta.ForeignKey(core.ContentType, name='content_type_id', rel_name='content_type', blank=True, null=True), - meta.TextField('object_id', blank=True, null=True), - meta.CharField('object_repr', maxlength=200), - meta.PositiveSmallIntegerField('action_flag'), - meta.TextField('change_message', blank=True), - ) - ordering = ('-action_time',) - module_constants = { - 'ADDITION': 1, - 'CHANGE': 2, - 'DELETION': 3, - } + action_time = meta.DateTimeField(auto_now=True) + user = meta.ForeignKey(User) + content_type = meta.ForeignKey(core.ContentType, blank=True, null=True) # TODO: content_type_id name? + object_id = meta.TextField(blank=True, null=True) + object_repr = meta.CharField(maxlength=200) + action_flag = meta.PositiveSmallIntegerField() + change_message = meta.TextField(blank=True) + class META: + module_name = 'log' + verbose_name_plural = 'log entries' + db_table = 'auth_admin_log' + ordering = ('-action_time',) + module_constants = { + 'ADDITION': 1, + 'CHANGE': 2, + 'DELETION': 3, + } def __repr__(self): return str(self.action_time) diff --git a/django/models/core.py b/django/models/core.py index e94a35b694..939373f7ec 100644 --- a/django/models/core.py +++ b/django/models/core.py @@ -1,12 +1,11 @@ from django.core import meta, validators class Site(meta.Model): - db_table = 'sites' - fields = ( - meta.CharField('domain', 'domain name', maxlength=100), - meta.CharField('name', 'display name', maxlength=50), - ) - ordering = ('domain',) + domain = meta.CharField('domain name', maxlength=100) + name = meta.CharField('display name', maxlength=50) + class META: + db_table = 'sites' + ordering = ('domain',) def __repr__(self): return self.domain @@ -17,25 +16,23 @@ class Site(meta.Model): return get_object(pk=SITE_ID) class Package(meta.Model): - db_table = 'packages' - fields = ( - meta.CharField('label', maxlength=20, primary_key=True), - meta.CharField('name', maxlength=30, unique=True), - ) - ordering = ('name',) + label = meta.CharField(maxlength=20, primary_key=True) + name = meta.CharField(maxlength=30, unique=True) + class META: + db_table = 'packages' + ordering = ('name',) def __repr__(self): return self.name class ContentType(meta.Model): - db_table = 'content_types' - fields = ( - meta.CharField('name', maxlength=100), - meta.ForeignKey(Package, name='package'), - meta.CharField('python_module_name', maxlength=50), - ) - ordering = ('package', 'name') - unique_together = (('package', 'python_module_name'),) + name = meta.CharField(maxlength=100) + package = meta.ForeignKey(Package, db_column='package') + python_module_name = meta.CharField(maxlength=50) + class META: + db_table = 'content_types' + ordering = ('package', 'name') + unique_together = (('package', 'python_module_name'),) def __repr__(self): return "%s | %s" % (self.package, self.name) @@ -54,49 +51,45 @@ class ContentType(meta.Model): return self.get_model_module().get_object(**kwargs) class Redirect(meta.Model): - db_table = 'redirects' - fields = ( - meta.ForeignKey(Site, radio_admin=meta.VERTICAL), - meta.CharField('old_path', 'redirect from', maxlength=200, db_index=True, - help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'."), - meta.CharField('new_path', 'redirect to', maxlength=200, blank=True, - help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'."), - ) - unique_together=(('site_id', 'old_path'),) - ordering = ('old_path',) - admin = meta.Admin( - list_display = ('__repr__',), - list_filter = ('site_id',), - search_fields = ('old_path', 'new_path'), - ) + site = meta.ForeignKey(Site, radio_admin=meta.VERTICAL) + old_path = meta.CharField('redirect from', maxlength=200, db_index=True, + help_text="This should be an absolute path, excluding the domain name. Example: '/events/search/'.") + new_path = meta.CharField('redirect to', maxlength=200, blank=True, + help_text="This can be either an absolute path (as above) or a full URL starting with 'http://'.") + class META: + db_table = 'redirects' + unique_together=(('site', 'old_path'),) + ordering = ('old_path',) + admin = meta.Admin( + list_filter = ('site',), + search_fields = ('old_path', 'new_path'), + ) def __repr__(self): return "%s ---> %s" % (self.old_path, self.new_path) class FlatFile(meta.Model): - db_table = 'flatfiles' - verbose_name = 'flat page' - fields = ( - meta.CharField('url', 'URL', maxlength=100, validator_list=[validators.isAlphaNumericURL], - help_text="Example: '/about/contact/'. Make sure to have leading and trailing slashes."), - meta.CharField('title', maxlength=200), - meta.TextField('content', help_text="Full HTML is allowed."), - meta.BooleanField('enable_comments'), - meta.CharField('template_name', maxlength=70, blank=True, - help_text="Example: 'flatfiles/contact_page'. If this isn't provided, the system will use 'flatfiles/default'."), - meta.BooleanField('registration_required', - help_text="If this is checked, only logged-in users will be able to view the page."), - meta.ManyToManyField(Site), - ) - ordering = ('url',) - admin = meta.Admin( - fields = ( - (None, {'fields': ('url', 'title', 'content', 'sites')}), - ('Advanced options', {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}), - ), - list_filter = ('sites',), - search_fields = ('url', 'title'), - ) + url = meta.CharField('URL', maxlength=100, validator_list=[validators.isAlphaNumericURL], + help_text="Example: '/about/contact/'. Make sure to have leading and trailing slashes.") + title = meta.CharField(maxlength=200) + content = meta.TextField() + enable_comments = meta.BooleanField() + template_name = meta.CharField(maxlength=70, blank=True, + help_text="Example: 'flatfiles/contact_page'. If this isn't provided, the system will use 'flatfiles/default'.") + registration_required = meta.BooleanField(help_text="If this is checked, only logged-in users will be able to view the page.") + sites = meta.ManyToManyField(Site) + class META: + db_table = 'flatfiles' + verbose_name = 'flat page' + ordering = ('url',) + admin = meta.Admin( + fields = ( + (None, {'fields': ('url', 'title', 'content', 'sites')}), + ('Advanced options', {'classes': 'collapse', 'fields': ('enable_comments', 'registration_required', 'template_name')}), + ), + list_filter = ('sites',), + search_fields = ('url', 'title'), + ) def __repr__(self): return "%s -- %s" % (self.url, self.title) @@ -108,18 +101,17 @@ import base64, md5, random, sys import cPickle as pickle class Session(meta.Model): - fields = ( - meta.CharField('session_key', maxlength=40, primary_key=True), - meta.TextField('session_data'), - meta.DateTimeField('expire_date'), - ) - module_constants = { - 'base64': base64, - 'md5': md5, - 'pickle': pickle, - 'random': random, - 'sys': sys, - } + session_key = meta.CharField(maxlength=40, primary_key=True) + session_data = meta.TextField() + expire_date = meta.DateTimeField() + class META: + module_constants = { + 'base64': base64, + 'md5': md5, + 'pickle': pickle, + 'random': random, + 'sys': sys, + } def get_decoded(self): from django.conf.settings import SECRET_KEY diff --git a/django/templatetags/log.py b/django/templatetags/log.py index 564d8fbfa0..c88e28dabe 100644 --- a/django/templatetags/log.py +++ b/django/templatetags/log.py @@ -11,23 +11,23 @@ class AdminLogNode(template.Node): def render(self, context): if self.user is not None and not self.user.isdigit(): self.user = context[self.user].id - context[self.varname] = log.get_list(user_id__exact=self.user, limit=self.limit, select_related=True) + context[self.varname] = log.get_list(user__id__exact=self.user, limit=self.limit, select_related=True) return '' class DoGetAdminLog: """ Populates a template variable with the admin log for the given criteria. - + Usage:: - + {% get_admin_log [limit] as [varname] for_user [context_var_containing_user_obj] %} - + Examples:: - + {% get_admin_log 10 as admin_log for_user 23 %} {% get_admin_log 10 as admin_log for_user user %} {% get_admin_log 10 as admin_log %} - + Note that ``context_var_containing_user_obj`` can be a hard-coded integer (user ID) or the name of a template context variable containing the user object whose ID you want. diff --git a/django/views/admin/main.py b/django/views/admin/main.py index c0a177a928..febe0adfad 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -387,12 +387,12 @@ def change_list(request, app_label, module_name): except ObjectDoesNotExist: result_repr = EMPTY_CHANGELIST_VALUE else: - field_val = getattr(result, f.name) + field_val = getattr(result, f.column) # Foreign-key fields are special: Use the repr of the # related object. if isinstance(f.rel, meta.ManyToOne): if field_val is not None: - result_repr = getattr(result, 'get_%s' % f.rel.name)() + result_repr = getattr(result, 'get_%s' % f.name)() else: result_repr = EMPTY_CHANGELIST_VALUE # Dates are special: They're formatted in a certain way. @@ -723,10 +723,10 @@ def _get_admin_field(field_list, name_prefix, rel, add, change): t.append('{{ %soriginal.%s }}' % ((rel and name_prefix or ''), field.name)) if change and use_raw_id_admin(field): if isinstance(field.rel, meta.ManyToOne): - if_bit = '%soriginal.get_%s' % (rel and name_prefix or '', field.rel.name) + if_bit = '%soriginal.get_%s' % (rel and name_prefix or '', field.name) obj_repr = if_bit + '|truncatewords:"14"' elif isinstance(field.rel, meta.ManyToMany): - if_bit = '%soriginal.get_%s_list' % (rel and name_prefix or '', field.rel.name) + if_bit = '%soriginal.get_%s_list' % (rel and name_prefix or '', field.name) obj_repr = if_bit + '|join:", "|truncatewords:"14"' t.append('{%% if %s %%} {{ %s }}{%% endif %%}' % (if_bit, obj_repr)) if field.help_text: @@ -915,21 +915,21 @@ 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.name))) + new_data.update(_get_flattened_data(f, getattr(obj, f.column))) for f in opts.many_to_many: if f.rel.raw_id_admin: - new_data[f.name] = ",".join([str(i.id) for i in getattr(obj, 'get_%s_list' % f.rel.name)()]) + new_data[f.name] = ",".join([str(i.id) for i in getattr(obj, 'get_%s_list' % f.rel.singular)()]) elif not f.rel.edit_inline: - new_data[f.name] = [i.id for i in getattr(obj, 'get_%s_list' % f.rel.name)()] + new_data[f.name] = [i.id for i in getattr(obj, 'get_%s_list' % f.rel.singular)()] for rel_obj, rel_field in inline_related_objects: var_name = rel_obj.object_name.lower() 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.name)).items(): + for k, v in _get_flattened_data(f, getattr(rel_instance, f.column)).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.name)] = [j.id for j in getattr(rel_instance, 'get_%s_list' % f.rel.name)()] + new_data['%s.%d.%s' % (var_name, i, f.column)] = [j.id for j in getattr(rel_instance, 'get_%s_list' % f.rel.singular)()] # If the object has ordered objects on its admin page, get the existing # order and flatten it into a comma-separated list of IDs. @@ -1095,7 +1095,7 @@ def delete_stage(request, app_label, module_name, object_id): def history(request, app_label, module_name, object_id): mod, opts = _get_mod_opts(app_label, module_name) - action_list = log.get_list(object_id__exact=object_id, content_type_id__exact=opts.get_content_type_id(), + action_list = log.get_list(object_id__exact=object_id, content_type__id__exact=opts.get_content_type_id(), order_by=("action_time",), select_related=True) # If no history was found, see whether this object even exists. try: diff --git a/django/views/defaults.py b/django/views/defaults.py index 4da2643c3d..c75bc6880e 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -43,13 +43,13 @@ def page_not_found(request): from django.conf.settings import APPEND_SLASH, SITE_ID path = request.get_full_path() try: - r = redirects.get_object(site_id__exact=SITE_ID, old_path__exact=path) + r = redirects.get_object(site__id__exact=SITE_ID, old_path__exact=path) except redirects.RedirectDoesNotExist: r = None if r is None and APPEND_SLASH: # Try removing the trailing slash. try: - r = redirects.get_object(site_id__exact=SITE_ID, old_path__exact=path[:path.rfind('/')]+path[path.rfind('/')+1:]) + r = redirects.get_object(site__id__exact=SITE_ID, old_path__exact=path[:path.rfind('/')]+path[path.rfind('/')+1:]) except redirects.RedirectDoesNotExist: pass if r is not None: diff --git a/docs/db-api.txt b/docs/db-api.txt index 40fe40a64d..3d9cc9b9c5 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -11,20 +11,16 @@ models, and how to create, retrieve, and update objects. Throughout this reference, we'll refer to the following Poll application:: class Poll(meta.Model): - fields = ( - meta.SlugField('slug', unique_for_month='pub_date'), - meta.CharField('question', maxlength=255), - meta.DateTimeField('pub_date'), - meta.DateTimeField('expire_date'), - ) + slug = meta.SlugField(unique_for_month='pub_date') + question = meta.CharField(maxlength=255) + pub_date = meta.DateTimeField() + expire_date = meta.DateTimeField() class Choice(meta.Model): - fields = ( - meta.ForeignKey(Poll, edit_inline=meta.TABULAR, - num_in_admin=10, min_num_in_admin=5), - meta.CharField('choice', maxlength=255, core=True), - meta.IntegerField('votes', editable=False, default=0), - ) + poll = meta.ForeignKey(Poll, edit_inline=meta.TABULAR, + num_in_admin=10, min_num_in_admin=5) + choice = meta.CharField(maxlength=255, core=True) + votes = meta.IntegerField(editable=False, default=0) Basic lookup functions ====================== @@ -163,23 +159,18 @@ automatically. One-to-one relations -------------------- -Each object in a one-to-one relationship will have a ``get_relatedobject()`` +Each object in a one-to-one relationship will have a ``get_relatedobjectname()`` method. For example:: class Place(meta.Model): - fields = ( - ... - ) + # ... class Restaurant(meta.Model): - ... - fields = ( - meta.OneToOneField(places.Place), - ... - ) + # ... + the_place = meta.OneToOneField(places.Place) In the above example, each ``Place`` will have a ``get_restaurant()`` method, -and each ``Restaurant`` will have a ``get_place()`` method. +and each ``Restaurant`` will have a ``get_theplace()`` method. Many-to-one relations --------------------- @@ -236,19 +227,15 @@ Note that ``select_related`` follows foreign keys as far as possible. If you hav following models:: class Poll(meta.Model): - ... + # ... class Choice(meta.Model): - fields = ( - meta.ForeignKey(Poll), - ... - ) + # ... + poll = meta.ForeignKey(Poll) class SingleVote(meta.Model): - fields = ( - meta.ForeignKey(Choice), - ... - ) + # ... + choice = meta.ForeignKey(Choice) then a call to ``singlevotes.get_object(id__exact=4, select_related=True)`` will cache the related choice *and* the related poll:: diff --git a/docs/faq.txt b/docs/faq.txt index d5400dd98c..e1f909cea3 100644 --- a/docs/faq.txt +++ b/docs/faq.txt @@ -291,14 +291,9 @@ dictionaries in order of query execution. Each dictionary has the following:: Can I use Django with a pre-existing database? ---------------------------------------------- -Yes. You have two options: +Yes. See `Integrating with a legacy database`_. - * Write models that describe your already-existing database layout, and - just point Django at your database. - * Use the alpha ``django-admin.py inspectdb`` function to automatically - create models by introspecting a given database. See `Ticket 90`_. - -.. _`Ticket 90`: http://code.djangoproject.com/ticket/90 +.. _`Integrating with a legacy database`: http://www.djangoproject.com/documentation/legacy_databases/ The admin site ============== diff --git a/docs/forms.txt b/docs/forms.txt index 5e2ddcdeed..e3a98feb32 100644 --- a/docs/forms.txt +++ b/docs/forms.txt @@ -26,14 +26,14 @@ this document, we'll be working with the following model, a "place" object:: ) class Place(meta.Model): - fields = ( - meta.CharField('name', maxlength=100), - meta.CharField('address', maxlength=100, blank=True), - meta.CharField('city', maxlength=50, blank=True), - meta.USStateField('state'), - meta.CharField('zip_code', maxlength=5, blank=True), - meta.IntegerField('place_type', choices=PLACE_TYPES) - ) + name = meta.CharField(maxlength=100), + address = meta.CharField(maxlength=100, blank=True), + city = meta.CharField(maxlength=50, blank=True), + state = meta.USStateField(), + zip_code = meta.CharField(maxlength=5, blank=True), + place_type = meta.IntegerField(choices=PLACE_TYPES) + class META: + admin = meta.Admin() def __repr__(self): return self.name diff --git a/docs/model-api.txt b/docs/model-api.txt index 02d7d7a509..bbe38929bf 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -6,15 +6,24 @@ Django's models are the bread and butter of the framework. There's a huge array of options available to you when defining your data models. This document explains them. -Options for models -================== +META options +============ -A list of all possible options for a model object follows. Although there's a -wide array of options, only ``fields`` is required. +Give your model metadata by using an inner ``"class META"``, like so:: + + class Foo(meta.Model): + bar = meta.CharField(maxlength=30) + # ... + class META: + admin = meta.Admin() + # ... + +Here's a list of all possible ``META`` options. No options are required. ``admin`` - A ``meta.Admin`` object; see `Admin options`_. If this field isn't given, - the object will not have an admin interface. + A ``meta.Admin`` object; see `Admin options`_. If this field is given, the + object will have an admin interface. If it isn't given, the object won't + have one. ``db_table`` The name of the database table to use for the module:: @@ -30,16 +39,6 @@ wide array of options, only ``fields`` is required. exceptions = ("DisgustingToppingsException", "BurntCrust") -``fields`` - A list of field objects. See `Field objects`_. For example:: - - fields = ( - meta.CharField('customer_name', maxlength=15), - meta.BooleanField('use_extra_cheese'), - meta.IntegerField('customer_type', choices=CUSTOMER_TYPE_CHOICES), - ... - ) - ``get_latest_by`` The name of a ``DateField`` or ``DateTimeField``; if given, the module will have a ``get_latest()`` function that fetches the "latest" object according @@ -69,7 +68,7 @@ wide array of options, only ``fields`` is required. respect to a parent object. For example, if a ``PizzaToppping`` relates to a ``Pizza`` object, you might use:: - order_with_respect_to = 'pizza_id' + order_with_respect_to = 'pizza' to allow the toppings to be ordered with respect to the associated pizza. @@ -95,7 +94,7 @@ wide array of options, only ``fields`` is required. ``unique_together`` Sets of field names that, taken together, must be unique:: - unique_together = (("driver_id", "restaurant_id"),) + unique_together = (("driver", "restaurant"),) This is a list of lists of fields that must be unique when considered together. It's used in the Django admin. @@ -118,18 +117,14 @@ wide array of options, only ``fields`` is required. Field objects ============= -The list of fields is the most important part of a data model. Each item in -the ``fields`` list is an instance of a ``meta.Field`` subclass and maps to -a database field. +The list of fields is the most important part of a data model. Each class +variable in a model, aside from the optional inner ``class META``, should be +an instance of a ``meta.Field`` subclass. -All field objects -- except for ``ForeignKey`` and ``ManyToManyField`` (see -below) -- require the field's machine-readable name as the first positional -argument. This must be a valid Python identifier -- no spaces, punctuation, -etc., are allowed. - -The second positional argument, a human-readable name, is optional. If the -human-readable name isn't given, Django will use the machine-readable name, -coverting underscores to spaces. +Each field type, except for ``ForeignKey``, ``ManyToManyField`` and +``OneToOneField``, takes an optional first positional argument, a +human-readable name. If the human-readable name isn't given, Django will use +the machine-readable name, converting underscores to spaces. General field options --------------------- @@ -173,6 +168,10 @@ common to all field types. These arguments are: It is an error to have an inline-editable relation without at least one core field. + ``db_column`` The name of the database column to use for this + field. If this isn't given, Django will use the + field's name. + ``db_index`` If ``True``, the SQL generator will create a database index on this field. @@ -229,7 +228,7 @@ Field Types use this directly; a primary key field will automatically be added to your model if you don't specify otherwise. That automatically-added field is:: - meta.AutoField('id', primary_key=True) + id = meta.AutoField(primary_key=True) ``BooleanField`` A true/false field. @@ -370,13 +369,6 @@ Field Types Not used with ``edit_inline``. - ``rel_name`` The name of the relation. In the above example, - this would default to 'pizza' (so that the - ``Toppings`` object would have a ``get_pizza()`` - function. If you set ``rel_name`` to "pie", then - the function would be called ``get_pie()`` and the - field name would be ``pie_id``. - ``related_name`` The name to use for the relation from the related object back to this one. For example, when if ``Topping`` has this field:: @@ -405,11 +397,9 @@ Field Types ... meta.ForeignKey(Category, name="primary_category_id", - rel_name="primary_category", related_name="primary_story"), meta.ForeignKey(Category, name="secondary_category_id", - rel_name="secondary_category", related_name="secondary_story"), ... @@ -445,24 +435,18 @@ Field Types ``core.flatfiles`` object:: class FlatFile(meta.Model): - fields = ( - ... - meta.ManyToManyField(Site), - ) + # ... + sites = meta.ManyToManyField(Site) Many-to-many relations are a bit different from other fields. First, they aren't actually a field per se, because they use a intermediary join table. Second, they don't take the same options as the rest of the fields. The - only arguments taken are: + first position argument is required and should be a model class. Other + available arguments, all of which are optional, are: ======================= ============================================================ Argument Description ======================= ============================================================ - ``rel_name`` Use this if you have more than one - ``ForeignKey`` in the same model that relate - to the same model. Django will use ``rel_name`` in - the generated API. - ``related_name`` See the description of ``related_name`` in ``ForeignKey``, above. @@ -475,14 +459,6 @@ Field Types ``limit_choices_to`` See the description under ``ForeignKey`` above. - ``name`` An alphanumeric name for the relationship. If this - isn't provided, Django uses the ``module_name`` of - the related object. - - This is only really useful when you have a single - object that relates to the same object more than - once. - ``verbose_name`` A human-readable name for the object, singular. If this isn't provided, Django uses the ``verbose_name`` for the related object. @@ -534,7 +510,7 @@ Field Types from which to auto-populate the slug, via JavaScript, in the object's admin form:: - meta.SlugField("slug", prepopulate_from=("pre_name", "name")), + meta.SlugField(prepopulate_from=("pre_name", "name")), ``SmallIntegerField`` Like an ``IntegerField``, but only allows values under a certain @@ -691,9 +667,7 @@ object's behavior. First, any methods you define will be available as methods of object instances. For example:: class Pizza(meta.Model): - fields = ( - ... - ) + # ... def is_disgusting(self): return "anchovies" in [topping.name for topping in self.get_topping_list()] @@ -742,9 +716,7 @@ that module. Any model method that begins with "_module_" is turned into a module-level function:: class Pizza(meta.Model): - fields = ( - ... - ) + # ... def _module_get_pizzas_to_deliver(): return get_list(delivered__exact=False) @@ -769,9 +741,7 @@ fields because manipulators automatically call any method that begins with "validate":: class Pizza(meta.Model): - fields = ( - ... - ) + # ... def _manipulator_validate_customer_id(self, field_data, all_data): from django.core import validators diff --git a/docs/overview.txt b/docs/overview.txt index 6917ef8a1d..90e3db9302 100644 --- a/docs/overview.txt +++ b/docs/overview.txt @@ -21,20 +21,16 @@ offers many rich ways of representing your models -- so far, it's been solving two years' worth of database-schema problems. Here's a quick example:: class Reporter(meta.Model): - fields = ( - meta.CharField('full_name', maxlength=70), - ) + full_name = meta.CharField(maxlength=70) def __repr__(self): return self.full_name class Article(meta.Model): - fields = ( - meta.DateTimeField('pub_date'), - meta.CharField('headline', maxlength=200), - meta.TextField('article'), - meta.ForeignKey(Reporter), - ) + pub_date = meta.DateTimeField('pub_date') + headline = meta.CharField('headline', maxlength=200) + article = meta.TextField('article') + reporter = meta.ForeignKey(Reporter) def __repr__(self): return self.headline @@ -134,17 +130,16 @@ A dynamic admin interface: It's not just scaffolding -- it's the whole house Once your models are defined, Django can automatically create an administrative interface -- a Web site that lets authenticated users add, change and -delete objects. It's as easy as adding an extra admin attribute to your model -classes:: +delete objects. It's as easy as adding an extra ``admin`` attribute to your +model classes:: class Article(meta.Model): - fields = ( - meta.DateTimeField('pub_date'), - meta.CharField('headline', maxlength=200), - meta.TextField('article'), - meta.ForeignKey(Reporter), - ) - admin = meta.Admin() + pub_date = meta.DateTimeField('pub_date') + headline = meta.CharField('headline', maxlength=200) + article = meta.TextField('article') + reporter = meta.ForeignKey(Reporter) + class META: + admin = meta.Admin() The philosophy here is that your site is edited by a staff, or a client, or maybe just you -- and you don't want to have to deal with creating backend diff --git a/docs/tutorial01.txt b/docs/tutorial01.txt index 112f5ca05c..6b4380290f 100644 --- a/docs/tutorial01.txt +++ b/docs/tutorial01.txt @@ -167,41 +167,36 @@ These concepts are represented by simple Python classes. Edit the from django.core import meta class Poll(meta.Model): - fields = ( - meta.CharField('question', maxlength=200), - meta.DateTimeField('pub_date', 'date published'), - ) + question = meta.CharField(maxlength=200) + pub_date = meta.DateTimeField('date published') class Choice(meta.Model): - fields = ( - meta.ForeignKey(Poll), - meta.CharField('choice', maxlength=200), - meta.IntegerField('votes'), - ) + poll = meta.ForeignKey(Poll) + choice = meta.CharField(maxlength=200) + votes = meta.IntegerField() The code is straightforward. Each model is represented by a class that -subclasses ``django.core.meta.Model``. Each model has a single class variable, -``fields``, which is a tuple of database fields in the model. +subclasses ``django.core.meta.Model``. Each model a number of class variables, +each of which represents a database field in the model. Each field is represented by an instance of a ``meta.*Field`` class -- e.g., ``meta.CharField`` for character fields and ``meta.DateTimeField`` for datetimes. This tells Django what type of data each field holds. -The first argument to each ``Field`` call is the field's name, in -machine-friendly format. You'll use this value in your Python code, and your -database will use it as the column name. +The name of each ``meta.*Field`` instance (e.g. ``question`` or ``pub_date`` ) +is the field's name, in machine-friendly format. You'll use this value in your +Python code, and your database will use it as the column name. -The second, optional, argument is the field's human-readable name. That's used -in a couple of introspective parts of Django, and it doubles as documentation. -If this field isn't provided, Django will use the machine-readable name. In -this example, we've only defined a human-readable name for ``Poll.pub_date``. -For all other fields in this model, the field's machine-readable name will -suffice as its human-readable name. +You can use an optional first positional argument to a ``Field`` to designate a +human-readable name. That's used in a couple of introspective parts of Django, +and it doubles as documentation. If this field isn't provided, Django will use +the machine-readable name. In this example, we've only defined a human-readable +name for ``Poll.pub_date``. For all other fields in this model, the field's +machine-readable name will suffice as its human-readable name. -Some ``meta.*Field`` classes have additional required elements. -``meta.CharField``, for example, requires that you give it a ``maxlength``. -That's used not only in the database schema, but in validation, as we'll soon -see. +Some ``meta.*Field`` classes have required elements. ``meta.CharField``, for +example, requires that you give it a ``maxlength``. That's used not only in the +database schema, but in validation, as we'll soon see. Finally, note a relationship is defined, using ``meta.ForeignKey``. That tells Django each Choice is related to a single Poll. Django supports all the common @@ -266,6 +261,9 @@ Note the following: * Primary keys (IDs) are added automatically. (You can override this, too.) + * Django appends ``"_id"`` to the foreign key field name, by convention. + Yes, you can override this, as well. + * The foreign key relationship is made explicit by a ``REFERENCES`` statement. * It's tailored to the database you're using, so database-specific field types diff --git a/docs/tutorial02.txt b/docs/tutorial02.txt index b42bafb374..e1d0401592 100644 --- a/docs/tutorial02.txt +++ b/docs/tutorial02.txt @@ -89,9 +89,7 @@ objects have an admin interface. Edit the ``myproject/apps/polls/models/polls.py file and make the following change to add an ``admin`` attribute:: class Poll(meta.Model): - fields = ( - # ... - ) + # ... admin = meta.Admin() Now reload the Django admin page to see your changes. Note that you don't have @@ -242,15 +240,15 @@ Poll object. Let's make that happen. Remove the ``admin`` for the Choice model. Then, edit the ``ForeignKey(Poll)`` field like so:: - meta.ForeignKey(Poll, edit_inline=meta.STACKED, num_in_admin=3), + poll = meta.ForeignKey(Poll, edit_inline=meta.STACKED, num_in_admin=3) This tells Django: "Choice objects are edited on the Poll admin page. By default, provide enough fields for 3 Choices." Then change the other fields in ``Choice`` to give them ``core=True``:: - meta.CharField('choice', 'choice', maxlength=200, core=True), - meta.IntegerField('votes', 'votes', core=True), + choice = meta.CharField(maxlength=200, core=True) + votes = meta.IntegerField(core=True) This tells Django: "When you edit a Choice on the Poll admin page, the 'choice' and 'votes' fields are required. The presence of at least one of them signifies @@ -274,7 +272,7 @@ One small problem, though. It takes a lot of screen space to display all the fields for entering related Choice objects. For that reason, Django offers an alternate way of displaying inline related objects:: - meta.ForeignKey(Poll, edit_inline=meta.TABULAR, num_in_admin=3), + poll = meta.ForeignKey(Poll, edit_inline=meta.TABULAR, num_in_admin=3) With that ``edit_inline=meta.TABULAR`` (instead of ``meta.STACKED``), the related objects are displayed in a more compact, table-based format: diff --git a/tests/testapp/models/__init__.py b/tests/testapp/models/__init__.py index d76059ec74..0fab083922 100644 --- a/tests/testapp/models/__init__.py +++ b/tests/testapp/models/__init__.py @@ -1,3 +1,4 @@ __all__ = ['basic', 'repr', 'custom_methods', 'many_to_one', 'many_to_many', 'ordering', 'lookup', 'get_latest', 'm2m_intermediary', 'one_to_one', - 'm2o_recursive', 'm2o_recursive2', 'save_delete_hooks', 'custom_pk'] + 'm2o_recursive', 'm2o_recursive2', 'save_delete_hooks', 'custom_pk', + 'subclassing', 'many_to_one_null'] diff --git a/tests/testapp/models/basic.py b/tests/testapp/models/basic.py index c51ceccb9d..ebd784137a 100644 --- a/tests/testapp/models/basic.py +++ b/tests/testapp/models/basic.py @@ -7,10 +7,8 @@ This is a basic model with only two non-primary-key fields. from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100, default='Default headline'), - meta.DateTimeField('pub_date'), - ) + headline = meta.CharField(maxlength=100, default='Default headline') + pub_date = meta.DateTimeField() API_TESTS = """ # No articles are in the system yet. diff --git a/tests/testapp/models/custom_methods.py b/tests/testapp/models/custom_methods.py index f674b9c286..4f175752b4 100644 --- a/tests/testapp/models/custom_methods.py +++ b/tests/testapp/models/custom_methods.py @@ -23,10 +23,8 @@ namespace as custom methods. from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateField('pub_date'), - ) + headline = meta.CharField(maxlength=100) + pub_date = meta.DateField() def __repr__(self): return self.headline diff --git a/tests/testapp/models/custom_pk.py b/tests/testapp/models/custom_pk.py index d7407404f8..b7ebd61d4b 100644 --- a/tests/testapp/models/custom_pk.py +++ b/tests/testapp/models/custom_pk.py @@ -11,12 +11,11 @@ fails. from django.core import meta class Employee(meta.Model): - fields = ( - meta.CharField('employee_code', maxlength=10, primary_key=True), - meta.CharField('first_name', maxlength=20), - meta.CharField('last_name', maxlength=20), - ) - ordering = ('last_name', 'first_name') + employee_code = meta.CharField(maxlength=10, primary_key=True) + first_name = meta.CharField(maxlength=20) + last_name = meta.CharField(maxlength=20) + class META: + ordering = ('last_name', 'first_name') def __repr__(self): return "%s %s" % (self.first_name, self.last_name) diff --git a/tests/testapp/models/get_latest.py b/tests/testapp/models/get_latest.py index 2bd5a6660b..86697e85a7 100644 --- a/tests/testapp/models/get_latest.py +++ b/tests/testapp/models/get_latest.py @@ -11,11 +11,10 @@ date farthest into the future." from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateTimeField('pub_date'), - ) - get_latest_by = 'pub_date' + headline = meta.CharField(maxlength=100) + pub_date = meta.DateTimeField() + class META: + get_latest_by = 'pub_date' def __repr__(self): return self.headline diff --git a/tests/testapp/models/lookup.py b/tests/testapp/models/lookup.py index 4567b1f106..bb8f4aaad1 100644 --- a/tests/testapp/models/lookup.py +++ b/tests/testapp/models/lookup.py @@ -7,11 +7,10 @@ This demonstrates features of the database API. from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateTimeField('pub_date'), - ) - ordering = ('-pub_date', 'headline') + headline = meta.CharField(maxlength=100) + pub_date = meta.DateTimeField() + class META: + ordering = ('-pub_date', 'headline') def __repr__(self): return self.headline diff --git a/tests/testapp/models/m2m_intermediary.py b/tests/testapp/models/m2m_intermediary.py index 30d0bb2ac2..2a20072e03 100644 --- a/tests/testapp/models/m2m_intermediary.py +++ b/tests/testapp/models/m2m_intermediary.py @@ -13,49 +13,43 @@ writer"). from django.core import meta class Reporter(meta.Model): - fields = ( - meta.CharField('first_name', maxlength=30), - meta.CharField('last_name', maxlength=30), - ) + first_name = meta.CharField(maxlength=30) + last_name = meta.CharField(maxlength=30) def __repr__(self): return "%s %s" % (self.first_name, self.last_name) class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateField('pub_date'), - ) + headline = meta.CharField(maxlength=100) + pub_date = meta.DateField() def __repr__(self): return self.headline class Writer(meta.Model): - fields = ( - meta.ForeignKey(Reporter), - meta.ForeignKey(Article), - meta.CharField('position', maxlength=100), - ) + reporter = meta.ForeignKey(Reporter) + article = meta.ForeignKey(Article) + position = meta.CharField(maxlength=100) def __repr__(self): return '%r (%s)' % (self.get_reporter(), self.position) API_TESTS = """ # Create a few Reporters. ->>> r1 = reporters.Reporter(id=None, first_name='John', last_name='Smith') +>>> r1 = reporters.Reporter(first_name='John', last_name='Smith') >>> r1.save() ->>> r2 = reporters.Reporter(id=None, first_name='Jane', last_name='Doe') +>>> r2 = reporters.Reporter(first_name='Jane', last_name='Doe') >>> r2.save() # Create an Article. >>> from datetime import datetime ->>> a = articles.Article(id=None, headline='This is a test', pub_date=datetime(2005, 7, 27)) +>>> a = articles.Article(headline='This is a test', pub_date=datetime(2005, 7, 27)) >>> a.save() # Create a few Writers. ->>> w1 = writers.Writer(id=None, reporter_id=r1.id, article_id=a.id, position='Main writer') +>>> w1 = writers.Writer(reporter=r1, article=a, position='Main writer') >>> w1.save() ->>> w2 = writers.Writer(id=None, reporter_id=r2.id, article_id=a.id, position='Contributor') +>>> w2 = writers.Writer(reporter=r2, article=a, position='Contributor') >>> w2.save() # Play around with the API. diff --git a/tests/testapp/models/m2o_recursive.py b/tests/testapp/models/m2o_recursive.py index a0d211fc2c..27d13b4e7e 100644 --- a/tests/testapp/models/m2o_recursive.py +++ b/tests/testapp/models/m2o_recursive.py @@ -7,29 +7,25 @@ To define a many-to-one relationship between a model and itself, use In this example, a ``Category`` is related to itself. That is, each ``Category`` has a parent ``Category``. -Because of this recursive relationship, we need to tell Django what the -relationships should be called. Set ``rel_name`` for this, and set -``related_name`` to designate what the reverse relationship is called. +Set ``related_name`` to designate what the reverse relationship is called. """ from django.core import meta class Category(meta.Model): - module_name = 'categories' - fields = ( - meta.CharField('name', maxlength=20), - meta.ForeignKey('self', null=True, - rel_name='parent', related_name='child'), - ) + name = meta.CharField(maxlength=20) + parent = meta.ForeignKey('self', null=True, related_name='child') + class META: + module_name = 'categories' def __repr__(self): return self.name API_TESTS = """ # Create a few Category objects. ->>> r = categories.Category(id=None, name='Root category', parent_id=None) +>>> r = categories.Category(id=None, name='Root category', parent=None) >>> r.save() ->>> c = categories.Category(id=None, name='Child category', parent_id=r.id) +>>> c = categories.Category(id=None, name='Child category', parent=r) >>> c.save() >>> r.get_child_list() diff --git a/tests/testapp/models/m2o_recursive2.py b/tests/testapp/models/m2o_recursive2.py index ae1407ad31..52aa0f8b69 100644 --- a/tests/testapp/models/m2o_recursive2.py +++ b/tests/testapp/models/m2o_recursive2.py @@ -4,36 +4,28 @@ In this example, a ``Person`` can have a ``mother`` and ``father`` -- both of which are other ``Person`` objects. -Because a ``Person`` has multiple relationships to ``Person``, we need to -distinguish the relationships. Set ``rel_name`` to tell Django what the -relationship should be called, because ``Person`` has two relationships to the -same model. Also, set ``related_name`` to designate what the reverse -relationship is called. +Set ``related_name`` to designate what the reverse relationship is called. """ from django.core import meta class Person(meta.Model): - fields = ( - meta.CharField('full_name', maxlength=20), - meta.ForeignKey('self', null=True, rel_name='mother', - related_name='mothers_child'), - meta.ForeignKey('self', null=True, rel_name='father', - related_name='fathers_child'), - ) + full_name = meta.CharField(maxlength=20) + mother = meta.ForeignKey('self', null=True, related_name='mothers_child') + father = meta.ForeignKey('self', null=True, related_name='fathers_child') def __repr__(self): return self.full_name API_TESTS = """ # Create two Person objects -- the mom and dad in our family. ->>> dad = persons.Person(id=None, full_name='John Smith Senior', mother_id=None, father_id=None) +>>> dad = persons.Person(full_name='John Smith Senior', mother=None, father=None) >>> dad.save() ->>> mom = persons.Person(id=None, full_name='Jane Smith', mother_id=None, father_id=None) +>>> mom = persons.Person(full_name='Jane Smith', mother=None, father=None) >>> mom.save() # Give mom and dad a kid. ->>> kid = persons.Person(id=None, full_name='John Smith Junior', mother_id=mom.id, father_id=dad.id) +>>> kid = persons.Person(full_name='John Smith Junior', mother=mom, father=dad) >>> kid.save() >>> kid.get_mother() diff --git a/tests/testapp/models/many_to_many.py b/tests/testapp/models/many_to_many.py index 283a004ca4..91addafe9b 100644 --- a/tests/testapp/models/many_to_many.py +++ b/tests/testapp/models/many_to_many.py @@ -10,18 +10,14 @@ and a publication has multiple articles. from django.core import meta class Publication(meta.Model): - fields = ( - meta.CharField('title', maxlength=30), - ) + title = meta.CharField(maxlength=30) def __repr__(self): return self.title class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.ManyToManyField(Publication), - ) + headline = meta.CharField(maxlength=100) + publications = meta.ManyToManyField(Publication) def __repr__(self): return self.headline diff --git a/tests/testapp/models/many_to_one.py b/tests/testapp/models/many_to_one.py index a5bb16cb06..91dae95614 100644 --- a/tests/testapp/models/many_to_one.py +++ b/tests/testapp/models/many_to_one.py @@ -1,42 +1,38 @@ """ 4. Many-to-one relationships -To define a many-to-one relationship, use ForeignKey(). +To define a many-to-one relationship, use ``ForeignKey()`` . """ from django.core import meta class Reporter(meta.Model): - fields = ( - meta.CharField('first_name', maxlength=30), - meta.CharField('last_name', maxlength=30), - ) + first_name = meta.CharField(maxlength=30) + last_name = meta.CharField(maxlength=30) def __repr__(self): return "%s %s" % (self.first_name, self.last_name) class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateField('pub_date'), - meta.ForeignKey(Reporter), - ) + headline = meta.CharField(maxlength=100) + pub_date = meta.DateField() + reporter = meta.ForeignKey(Reporter) def __repr__(self): return self.headline API_TESTS = """ # Create a Reporter. ->>> r = reporters.Reporter(id=None, first_name='John', last_name='Smith') +>>> r = reporters.Reporter(first_name='John', last_name='Smith') >>> r.save() # Create an Article. >>> from datetime import datetime ->>> a = articles.Article(id=None, headline='This is a test', pub_date=datetime(2005, 7, 27), reporter_id=r.id) +>>> a = articles.Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter=r) >>> a.save() >>> a.reporter_id -1L +1 >>> a.get_reporter() John Smith @@ -47,7 +43,7 @@ John Smith ('John', 'Smith') # Create an Article via the Reporter object. ->>> new_article = r.add_article(headline="John's second story", pub_date=datetime(2005, 7, 28)) +>>> new_article = r.add_article(headline="John's second story", pub_date=datetime(2005, 7, 29)) >>> new_article John's second story >>> new_article.reporter_id @@ -61,7 +57,7 @@ John's second story This is a test >>> r.get_article_count() -2L +2 # The API automatically follows relationships as far as you need. # Use double underscores to separate relationships. @@ -70,4 +66,32 @@ This is a test >>> articles.get_list(reporter__first_name__exact='John', order_by=['pub_date']) [This is a test, John's second story] +# Find all Articles for the Reporter whose ID is 1. +>>> articles.get_list(reporter__id__exact=1, order_by=['pub_date']) +[This is a test, John's second story] + +# Note you need two underscores between "reporter" and "id" -- not one. +>>> articles.get_list(reporter_id__exact=1) +Traceback (most recent call last): + ... +TypeError: got unexpected keyword argument 'reporter_id__exact' + +# "pk" shortcut syntax works in a related context, too. +>>> articles.get_list(reporter__pk=1, order_by=['pub_date']) +[This is a test, John's second story] + +# You can also instantiate an Article by passing +# the Reporter's ID instead of a Reporter object. +>>> a3 = articles.Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter_id=r.id) +>>> a3.save() +>>> a3.reporter_id +1 +>>> a3.get_reporter() +John Smith + +# Similarly, the reporter ID can be a string. +>>> a4 = articles.Article(id=None, headline="This is a test", pub_date=datetime(2005, 7, 27), reporter_id="1") +>>> a4.save() +>>> a4.get_reporter() +John Smith """ diff --git a/tests/testapp/models/many_to_one_null.py b/tests/testapp/models/many_to_one_null.py new file mode 100644 index 0000000000..dcf6373e68 --- /dev/null +++ b/tests/testapp/models/many_to_one_null.py @@ -0,0 +1,77 @@ +""" +16. Many-to-one relationships that can be null + +To define a many-to-one relationship, use ``ForeignKey()`` with ``null=True`` . +""" + +from django.core import meta + +class Reporter(meta.Model): + name = meta.CharField(maxlength=30) + + def __repr__(self): + return self.name + +class Article(meta.Model): + headline = meta.CharField(maxlength=100) + reporter = meta.ForeignKey(Reporter, null=True) + + def __repr__(self): + return self.headline + +API_TESTS = """ +# Create a Reporter. +>>> r = reporters.Reporter(name='John Smith') +>>> r.save() + +# Create an Article. +>>> a = articles.Article(headline="First", reporter=r) +>>> a.save() + +>>> a.reporter_id +1 + +>>> a.get_reporter() +John Smith + +# Article objects have access to their related Reporter objects. +>>> r = a.get_reporter() + +# Create an Article via the Reporter object. +>>> a2 = r.add_article(headline="Second") +>>> a2 +Second +>>> a2.reporter_id +1 + +# Reporter objects have access to their related Article objects. +>>> r.get_article_list(order_by=['headline']) +[First, Second] +>>> r.get_article(headline__startswith='Fir') +First +>>> r.get_article_count() +2 + +# Create an Article with no Reporter by passing "reporter=None". +>>> a3 = articles.Article(headline="Third", reporter=None) +>>> a3.save() +>>> a3.id +3 +>>> a3.reporter_id +>>> print a3.reporter_id +None +>>> a3 = articles.get_object(pk=3) +>>> print a3.reporter_id +None + +# An article's get_reporter() method throws ReporterDoesNotExist +# if the reporter is set to None. +>>> a3.get_reporter() +Traceback (most recent call last): + ... +ReporterDoesNotExist + +# To retrieve the articles with no reporters set, use "reporter__isnull=True". +>>> articles.get_list(reporter__isnull=True) +[Third] +""" diff --git a/tests/testapp/models/one_to_one.py b/tests/testapp/models/one_to_one.py index d150daea8e..51b3efe733 100644 --- a/tests/testapp/models/one_to_one.py +++ b/tests/testapp/models/one_to_one.py @@ -9,33 +9,29 @@ In this example, a ``Place`` optionally can be a ``Restaurant``. from django.core import meta class Place(meta.Model): - fields = ( - meta.CharField('name', maxlength=50), - meta.CharField('address', maxlength=80), - ) + name = meta.CharField(maxlength=50) + address = meta.CharField(maxlength=80) def __repr__(self): return "%s the place" % self.name class Restaurant(meta.Model): - fields = ( - meta.OneToOneField(Place), - meta.BooleanField('serves_hot_dogs'), - meta.BooleanField('serves_pizza'), - ) + place = meta.OneToOneField(Place) + serves_hot_dogs = meta.BooleanField() + serves_pizza = meta.BooleanField() def __repr__(self): return "%s the restaurant" % self.get_place().name API_TESTS = """ # Create a couple of Places. ->>> p1 = places.Place(id=None, name='Demon Dogs', address='944 W. Fullerton') +>>> p1 = places.Place(name='Demon Dogs', address='944 W. Fullerton') >>> p1.save() ->>> p2 = places.Place(id=None, name='Ace Hardware', address='1013 N. Ashland') +>>> p2 = places.Place(name='Ace Hardware', address='1013 N. Ashland') >>> p2.save() # Create a Restaurant. Pass the ID of the "parent" object as this object's ID. ->>> r = restaurants.Restaurant(id=p1.id, serves_hot_dogs=True, serves_pizza=False) +>>> r = restaurants.Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False) >>> r.save() # A Restaurant can access its place. @@ -50,7 +46,7 @@ Demon Dogs the restaurant >>> p2.get_restaurant() Traceback (most recent call last): ... -RestaurantDoesNotExist: Restaurant does not exist for {'id__exact': ...} +RestaurantDoesNotExist: Restaurant does not exist for {'place__id__exact': ...} # restaurants.get_list() just returns the Restaurants, not the Places. >>> restaurants.get_list() @@ -60,4 +56,9 @@ RestaurantDoesNotExist: Restaurant does not exist for {'id__exact': ...} # Restaurants. >>> places.get_list(order_by=['name']) [Ace Hardware the place, Demon Dogs the place] + +>>> restaurants.get_object(place__id__exact=1) +Demon Dogs the restaurant +>>> restaurants.get_object(pk=1) +Demon Dogs the restaurant """ diff --git a/tests/testapp/models/ordering.py b/tests/testapp/models/ordering.py index 0efa0638df..b7b68b3158 100644 --- a/tests/testapp/models/ordering.py +++ b/tests/testapp/models/ordering.py @@ -16,11 +16,10 @@ undefined -- not random, just undefined. from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateTimeField('pub_date'), - ) - ordering = ('-pub_date', 'headline') + headline = meta.CharField(maxlength=100) + pub_date = meta.DateTimeField() + class META: + ordering = ('-pub_date', 'headline') def __repr__(self): return self.headline @@ -28,13 +27,13 @@ class Article(meta.Model): API_TESTS = """ # Create a couple of Articles. >>> from datetime import datetime ->>> a1 = articles.Article(id=None, headline='Article 1', pub_date=datetime(2005, 7, 26)) +>>> a1 = articles.Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) >>> a1.save() ->>> a2 = articles.Article(id=None, headline='Article 2', pub_date=datetime(2005, 7, 27)) +>>> a2 = articles.Article(headline='Article 2', pub_date=datetime(2005, 7, 27)) >>> a2.save() ->>> a3 = articles.Article(id=None, headline='Article 3', pub_date=datetime(2005, 7, 27)) +>>> a3 = articles.Article(headline='Article 3', pub_date=datetime(2005, 7, 27)) >>> a3.save() ->>> a4 = articles.Article(id=None, headline='Article 4', pub_date=datetime(2005, 7, 28)) +>>> a4 = articles.Article(headline='Article 4', pub_date=datetime(2005, 7, 28)) >>> a4.save() # By default, articles.get_list() orders by pub_date descending, then diff --git a/tests/testapp/models/repr.py b/tests/testapp/models/repr.py index 00f78f967f..3d4daf22c9 100644 --- a/tests/testapp/models/repr.py +++ b/tests/testapp/models/repr.py @@ -11,10 +11,8 @@ automatically-generated admin. from django.core import meta class Article(meta.Model): - fields = ( - meta.CharField('headline', maxlength=100), - meta.DateTimeField('pub_date'), - ) + headline = meta.CharField(maxlength=100) + pub_date = meta.DateTimeField() def __repr__(self): return self.headline @@ -22,7 +20,7 @@ class Article(meta.Model): API_TESTS = """ # Create an Article. >>> from datetime import datetime ->>> a = articles.Article(id=None, headline='Area man programs in Python', pub_date=datetime(2005, 7, 28)) +>>> a = articles.Article(headline='Area man programs in Python', pub_date=datetime(2005, 7, 28)) >>> a.save() >>> repr(a) diff --git a/tests/testapp/models/save_delete_hooks.py b/tests/testapp/models/save_delete_hooks.py index 3c895b8bee..f0fa836f71 100644 --- a/tests/testapp/models/save_delete_hooks.py +++ b/tests/testapp/models/save_delete_hooks.py @@ -13,10 +13,8 @@ Django provides hooks for executing arbitrary code around ``save()`` and from django.core import meta class Person(meta.Model): - fields = ( - meta.CharField('first_name', maxlength=20), - meta.CharField('last_name', maxlength=20), - ) + first_name = meta.CharField(maxlength=20) + last_name = meta.CharField(maxlength=20) def __repr__(self): return "%s %s" % (self.first_name, self.last_name) diff --git a/tests/testapp/models/subclassing.py b/tests/testapp/models/subclassing.py new file mode 100644 index 0000000000..7ec8d37ff9 --- /dev/null +++ b/tests/testapp/models/subclassing.py @@ -0,0 +1,168 @@ +""" +15. Subclassing models + +You can subclass another model to create a copy of it that behaves slightly +differently. +""" + +from django.core import meta + +# From the "Bare-bones model" example +from django.models.basic import Article + +# From the "Adding __repr__()" example +from django.models.repr import Article as ArticleWithRepr + +# From the "Specifying ordering" example +from django.models.ordering import Article as ArticleWithOrdering + +# This uses all fields and metadata from Article and +# adds a "section" field. +class ArticleWithSection(Article): + section = meta.CharField(maxlength=30) + class META: + module_name = 'subarticles1' + +# This uses all fields and metadata from Article but +# removes the "pub_date" field. +class ArticleWithoutPubDate(Article): + class META: + module_name = 'subarticles2' + remove_fields = ('pub_date',) + +# This uses all fields and metadata from Article but +# overrides the "pub_date" field. +class ArticleWithFieldOverride(Article): + pub_date = meta.DateField() # overrides the old field, a DateTimeField + class META: + module_name = 'subarticles3' + # No need to add remove_fields = ('pub_date',) + +# This uses all fields and metadata from ArticleWithRepr and +# makes a few additions/changes. +class ArticleWithManyChanges(ArticleWithRepr): + section = meta.CharField(maxlength=30) + is_popular = meta.BooleanField() + pub_date = meta.DateField() # overrides the old field, a DateTimeField + class META: + module_name = 'subarticles4' + +# This uses all fields from ArticleWithOrdering but +# changes the ordering parameter. +class ArticleWithChangedMeta(ArticleWithOrdering): + class META: + module_name = 'subarticles5' + ordering = ('headline', 'pub_date') + +API_TESTS = """ +# No data is in the system yet. +>>> subarticles1.get_list() +[] +>>> subarticles2.get_list() +[] +>>> subarticles3.get_list() +[] + +# Create an ArticleWithSection. +>>> from datetime import date, datetime +>>> a1 = subarticles1.ArticleWithSection(headline='First', pub_date=datetime(2005, 8, 22), section='News') +>>> a1.save() +>>> a1 + +>>> a1.id +1 +>>> a1.headline +'First' +>>> a1.pub_date +datetime.datetime(2005, 8, 22, 0, 0) + +# Retrieve it again, to prove the fields have been saved. +>>> a1 = subarticles1.get_object(pk=1) +>>> a1.headline +'First' +>>> a1.pub_date +datetime.datetime(2005, 8, 22, 0, 0) +>>> a1.section +'News' + +# Create an ArticleWithoutPubDate. +>>> a2 = subarticles2.ArticleWithoutPubDate(headline='Second') +>>> a2.save() +>>> a2 + +>>> a2.id +1 +>>> a2.pub_date +Traceback (most recent call last): + ... +AttributeError: 'ArticleWithoutPubDate' object has no attribute 'pub_date' + +# Retrieve it again, to prove the fields have been saved. +>>> a2 = subarticles2.get_object(pk=1) +>>> a2.headline +'Second' +>>> a2.pub_date +Traceback (most recent call last): + ... +AttributeError: 'ArticleWithoutPubDate' object has no attribute 'pub_date' + +# Create an ArticleWithFieldOverride. +>>> a3 = subarticles3.ArticleWithFieldOverride(headline='Third', pub_date=date(2005, 8, 22)) +>>> a3.save() +>>> a3 + +>>> a3.id +1 +>>> a3.pub_date +datetime.date(2005, 8, 22) + +# Retrieve it again, to prove the fields have been saved. +>>> a3 = subarticles3.get_object(pk=1) +>>> a3.headline +'Third' +>>> a3.pub_date +datetime.date(2005, 8, 22) + +# Create an ArticleWithManyChanges. +>>> a4 = subarticles4.ArticleWithManyChanges(headline='Fourth', section='Arts', +... is_popular=True, pub_date=date(2005, 8, 22)) +>>> a4.save() + +# a4 inherits __repr__() from its parent model (ArticleWithRepr). +>>> a4 +Fourth + +# Retrieve it again, to prove the fields have been saved. +>>> a4 = subarticles4.get_object(pk=1) +>>> a4.headline +'Fourth' +>>> a4.section +'Arts' +>>> a4.is_popular == True +True +>>> a4.pub_date +datetime.date(2005, 8, 22) + +# Test get_list(). +>>> subarticles1.get_list() +[] +>>> subarticles2.get_list() +[] +>>> subarticles3.get_list() +[] +>>> subarticles4.get_list() +[Fourth] + +# Create a couple of ArticleWithChangedMeta objects. +>>> a5 = subarticles5.ArticleWithChangedMeta(headline='A', pub_date=datetime(2005, 3, 1)) +>>> a5.save() +>>> a6 = subarticles5.ArticleWithChangedMeta(headline='B', pub_date=datetime(2005, 4, 1)) +>>> a6.save() +>>> a7 = subarticles5.ArticleWithChangedMeta(headline='C', pub_date=datetime(2005, 5, 1)) +>>> a7.save() + +# Ordering has been overridden, so objects are ordered +# by headline ASC instead of pub_date DESC. +>>> subarticles5.get_list() +[A, B, C] +"""