diff options
Diffstat (limited to 'mediagoblin/db')
-rw-r--r-- | mediagoblin/db/migrations.py | 61 | ||||
-rw-r--r-- | mediagoblin/db/mixin.py | 76 | ||||
-rw-r--r-- | mediagoblin/db/models.py | 58 |
3 files changed, 152 insertions, 43 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 3f43c789..167c4f87 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -21,6 +21,7 @@ from sqlalchemy import (MetaData, Table, Column, Boolean, SmallInteger, ForeignKey) from sqlalchemy.exc import ProgrammingError from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql import and_ from migrate.changeset.constraint import UniqueConstraint from mediagoblin.db.migration_tools import RegisterMigration, inspect_table @@ -190,9 +191,63 @@ def fix_CollectionItem_v0_constraint(db_conn): def add_license_preference(db): metadata = MetaData(bind=db.bind) - user_table = Table('core__users', metadata, autoload=True, - autoload_with=db.bind) + user_table = inspect_table(metadata, 'core__users') - col = Column('license_preference', Unicode, default=u'') + col = Column('license_preference', Unicode) col.create(user_table) db.commit() + + +@RegisterMigration(9, MIGRATIONS) +def mediaentry_new_slug_era(db): + """ + Update for the new era for media type slugs. + + Entries without slugs now display differently in the url like: + /u/cwebber/m/id=251/ + + ... because of this, we should back-convert: + - entries without slugs should be converted to use the id, if possible, to + make old urls still work + - slugs with = (or also : which is now also not allowed) to have those + stripped out (small possibility of breakage here sadly) + """ + import uuid + + def slug_and_user_combo_exists(slug, uploader): + return db.execute( + media_table.select( + and_(media_table.c.uploader==uploader, + media_table.c.slug==slug))).first() is not None + + def append_garbage_till_unique(row, new_slug): + """ + Attach junk to this row until it's unique, then save it + """ + if slug_and_user_combo_exists(new_slug, row.uploader): + # okay, still no success; + # let's whack junk on there till it's unique. + new_slug += '-' + uuid.uuid4().hex[:4] + # keep going if necessary! + while slug_and_user_combo_exists(new_slug, row.uploader): + new_slug += uuid.uuid4().hex[:4] + + db.execute( + media_table.update(). \ + where(media_table.c.id==row.id). \ + values(slug=new_slug)) + + metadata = MetaData(bind=db.bind) + + media_table = inspect_table(metadata, 'core__media_entries') + + for row in db.execute(media_table.select()): + # no slug, try setting to an id + if not row.slug: + append_garbage_till_unique(row, unicode(row.id)) + # has "=" or ":" in it... we're getting rid of those + elif u"=" in row.slug or u":" in row.slug: + append_garbage_till_unique( + row, row.slug.replace(u"=", u"-").replace(u":", u"-")) + + db.commit() diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py index 001b7826..6789a970 100644 --- a/mediagoblin/db/mixin.py +++ b/mediagoblin/db/mixin.py @@ -27,6 +27,8 @@ These functions now live here and get "mixed in" into the real objects. """ +import uuid + from werkzeug.utils import cached_property from mediagoblin import mg_globals @@ -52,19 +54,69 @@ class UserMixin(object): class MediaEntryMixin(object): def generate_slug(self): + """ + Generate a unique slug for this MediaEntry. + + This one does not *force* slugs, but usually it will probably result + in a niceish one. + + The end *result* of the algorithm will result in these resolutions for + these situations: + - If we have a slug, make sure it's clean and sanitized, and if it's + unique, we'll use that. + - If we have a title, slugify it, and if it's unique, we'll use that. + - If we can't get any sort of thing that looks like it'll be a useful + slug out of a title or an existing slug, bail, and don't set the + slug at all. Don't try to create something just because. Make + sure we have a reasonable basis for a slug first. + - If we have a reasonable basis for a slug (either based on existing + slug or slugified title) but it's not unique, first try appending + the entry's id, if that exists + - If that doesn't result in something unique, tack on some randomly + generated bits until it's unique. That'll be a little bit of junk, + but at least it has the basis of a nice slug. + """ # import this here due to a cyclic import issue # (db.models -> db.mixin -> db.util -> db.models) from mediagoblin.db.util import check_media_slug_used - self.slug = slugify(self.title) - - duplicate = check_media_slug_used(self.uploader, self.slug, self.id) - - if duplicate: - if self.id is not None: - self.slug = u"%s-%s" % (self.id, self.slug) - else: - self.slug = None + #Is already a slug assigned? Check if it is valid + if self.slug: + self.slug = slugify(self.slug) + + # otherwise, try to use the title. + elif self.title: + # assign slug based on title + self.slug = slugify(self.title) + + # We don't want any empty string slugs + if self.slug == u"": + self.slug = None + + # Do we have anything at this point? + # If not, we're not going to get a slug + # so just return... we're not going to force one. + if not self.slug: + return # giving up! + + # Otherwise, let's see if this is unique. + if check_media_slug_used(self.uploader, self.slug, self.id): + # It looks like it's being used... lame. + + # Can we just append the object's id to the end? + if self.id: + slug_with_id = u"%s-%s" % (self.slug, self.id) + if not check_media_slug_used(self.uploader, + slug_with_id, self.id): + self.slug = slug_with_id + return # success! + + # okay, still no success; + # let's whack junk on there till it's unique. + self.slug += '-' + uuid.uuid4().hex[:4] + # keep going if necessary! + while check_media_slug_used(self.uploader, self.slug, self.id): + self.slug += uuid.uuid4().hex[:4] @property def description_html(self): @@ -98,8 +150,10 @@ class MediaEntryMixin(object): @property def slug_or_id(self): - return (self.slug or self.id) - + if self.slug: + return self.slug + else: + return u'id:%s' % self.id def url_for_self(self, urlgen, **extra_args): """ diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 7e2cc7d2..2f58503f 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -20,7 +20,6 @@ TODO: indexes on foreignkeys, where useful. import logging import datetime -import sys from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \ Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \ @@ -32,9 +31,10 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.util import memoized_property from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded -from mediagoblin.db.base import Base, DictReadAttrProxy, Session +from mediagoblin.db.base import Base, DictReadAttrProxy from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin from mediagoblin.tools.files import delete_media_files +from mediagoblin.tools.common import import_component # It's actually kind of annoying how sqlalchemy-migrate does this, if # I understand it right, but whatever. Anyway, don't remove this :P @@ -84,9 +84,7 @@ class User(Base, UserMixin): def delete(self, **kwargs): """Deletes a User and all related entries/comments/files/...""" - # Delete this user's Collections and all contained CollectionItems - for collection in self.collections: - collection.delete(commit=False) + # Collections get deleted by relationships. media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id) for media in media_entries: @@ -147,6 +145,7 @@ class MediaEntry(Base, MediaEntryMixin): ) attachment_files_helper = relationship("MediaAttachmentFile", + cascade="all, delete-orphan", order_by="MediaAttachmentFile.created" ) attachment_files = association_proxy("attachment_files_helper", "dict_view", @@ -167,7 +166,6 @@ class MediaEntry(Base, MediaEntryMixin): collections = association_proxy("collections_helper", "in_collection") ## TODO - # media_data # fail_error def get_comments(self, ascending=False): @@ -197,40 +195,31 @@ class MediaEntry(Base, MediaEntryMixin): if media is not None: return media.url_for_self(urlgen) - #@memoized_property @property def media_data(self): - session = Session() - - return session.query(self.media_data_table).filter_by( - media_entry=self.id).first() + return getattr(self, self.media_data_ref) def media_data_init(self, **kwargs): """ Initialize or update the contents of a media entry's media_data row """ - session = Session() - - media_data = session.query(self.media_data_table).filter_by( - media_entry=self.id).first() + media_data = self.media_data - # No media data, so actually add a new one if media_data is None: - media_data = self.media_data_table( - media_entry=self.id, - **kwargs) - session.add(media_data) - # Update old media data + # Get the correct table: + table = import_component(self.media_type + '.models:DATA_MODEL') + # No media data, so actually add a new one + media_data = table(**kwargs) + # Get the relationship set up. + media_data.get_media_entry = self else: + # Update old media data for field, value in kwargs.iteritems(): setattr(media_data, field, value) @memoized_property - def media_data_table(self): - # TODO: memoize this - models_module = self.media_type + '.models' - __import__(models_module) - return sys.modules[models_module].DATA_MODEL + def media_data_ref(self): + return import_component(self.media_type + '.models:BACKREF_NAME') def __repr__(self): safe_title = self.title.encode('ascii', 'replace') @@ -395,7 +384,13 @@ class MediaComment(Base, MediaCommentMixin): created = Column(DateTime, nullable=False, default=datetime.datetime.now) content = Column(UnicodeText, nullable=False) - get_author = relationship(User) + # Cascade: Comments are owned by their creator. So do the full thing. + # lazy=dynamic: People might post a *lot* of comments, so make + # the "posted_comments" a query-like thing. + get_author = relationship(User, + backref=backref("posted_comments", + lazy="dynamic", + cascade="all, delete-orphan")) class Collection(Base, CollectionMixin): @@ -415,7 +410,10 @@ class Collection(Base, CollectionMixin): # TODO: No of items in Collection. Badly named, can we migrate to num_items? items = Column(Integer, default=0) - get_creator = relationship(User, backref="collections") + # Cascade: Collections are owned by their creator. So do the full thing. + get_creator = relationship(User, + backref=backref("collections", + cascade="all, delete-orphan")) def get_collection_items(self, ascending=False): #TODO, is this still needed with self.collection_items being available? @@ -436,7 +434,9 @@ class CollectionItem(Base, CollectionItemMixin): note = Column(UnicodeText, nullable=True) added = Column(DateTime, nullable=False, default=datetime.datetime.now) position = Column(Integer) - in_collection = relationship("Collection", + + # Cascade: CollectionItems are owned by their Collection. So do the full thing. + in_collection = relationship(Collection, backref=backref( "collection_items", cascade="all, delete-orphan")) |