diff options
Diffstat (limited to 'mediagoblin/db')
-rw-r--r-- | mediagoblin/db/migration_tools.py | 8 | ||||
-rw-r--r-- | mediagoblin/db/migrations.py | 61 | ||||
-rw-r--r-- | mediagoblin/db/mixin.py | 133 | ||||
-rw-r--r-- | mediagoblin/db/models.py | 58 | ||||
-rw-r--r-- | mediagoblin/db/util.py | 2 |
5 files changed, 192 insertions, 70 deletions
diff --git a/mediagoblin/db/migration_tools.py b/mediagoblin/db/migration_tools.py index e5380a3b..c0c7e998 100644 --- a/mediagoblin/db/migration_tools.py +++ b/mediagoblin/db/migration_tools.py @@ -17,6 +17,9 @@ from mediagoblin.tools.common import simple_printer from sqlalchemy import Table +class TableAlreadyExists(Exception): + pass + class MigrationManager(object): """ @@ -128,7 +131,10 @@ class MigrationManager(object): # sanity check before we proceed, none of these should be created for model in self.models: # Maybe in the future just print out a "Yikes!" or something? - assert not model.__table__.exists(self.session.bind) + if model.__table__.exists(self.session.bind): + raise TableAlreadyExists( + u"Intended to create table '%s' but it already exists" % + model.__table__.name) self.migration_model.metadata.create_all( self.session.bind, 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..0dc3bc85 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 @@ -50,21 +52,83 @@ class UserMixin(object): return cleaned_markdown_conversion(self.bio) -class MediaEntryMixin(object): +class GenerateSlugMixin(object): + """ + Mixin to add a generate_slug method to objects. + + Depends on: + - self.slug + - self.title + - self.check_slug_used(new_slug) + """ def generate_slug(self): + """ + Generate a unique slug for this object. + + 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. + """ + #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 self.check_slug_used(self.slug): + # 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 self.check_slug_used(slug_with_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 self.check_slug_used(self.slug): + self.slug += uuid.uuid4().hex[:4] + + +class MediaEntryMixin(GenerateSlugMixin): + def check_slug_used(self, 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 + return check_media_slug_used(self.uploader, slug, self.id) @property def description_html(self): @@ -74,32 +138,38 @@ class MediaEntryMixin(object): """ return cleaned_markdown_conversion(self.description) - def get_display_media(self, media_map, - fetch_order=common.DISPLAY_IMAGE_FETCHING_ORDER): - """ - Find the best media for display. + def get_display_media(self): + """Find the best media for display. - Args: - - media_map: a dict like - {u'image_size': [u'dir1', u'dir2', u'image.jpg']} - - fetch_order: the order we should try fetching images in + We try checking self.media_manager.fetching_order if it exists to + pull down the order. Returns: - (media_size, media_path) + (media_size, media_path) + or, if not found, None. + """ - media_sizes = media_map.keys() + fetch_order = self.media_manager.get("media_fetch_order") - for media_size in common.DISPLAY_IMAGE_FETCHING_ORDER: + # No fetching order found? well, give up! + if not fetch_order: + return None + + media_sizes = self.media_files.keys() + + for media_size in fetch_order: if media_size in media_sizes: - return media_map[media_size] + return media_size, self.media_files[media_size] def main_mediafile(self): pass @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): """ @@ -180,22 +250,13 @@ class MediaCommentMixin(object): return cleaned_markdown_conversion(self.content) -class CollectionMixin(object): - def generate_slug(self): +class CollectionMixin(GenerateSlugMixin): + def check_slug_used(self, slug): # import this here due to a cyclic import issue # (db.models -> db.mixin -> db.util -> db.models) from mediagoblin.db.util import check_collection_slug_used - self.slug = slugify(self.title) - - duplicate = check_collection_slug_used(mg_globals.database, - self.creator, 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 + return check_collection_slug_used(self.creator, slug, self.id) @property def description_html(self): 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")) diff --git a/mediagoblin/db/util.py b/mediagoblin/db/util.py index 529ef8b9..6ffec44d 100644 --- a/mediagoblin/db/util.py +++ b/mediagoblin/db/util.py @@ -59,7 +59,7 @@ def clean_orphan_tags(commit=True): Session.commit() -def check_collection_slug_used(dummy_db, creator_id, slug, ignore_c_id): +def check_collection_slug_used(creator_id, slug, ignore_c_id): filt = (Collection.creator == creator_id) \ & (Collection.slug == slug) if ignore_c_id is not None: |