TODO: indexes on foreignkeys, where useful.
"""
-
+import logging
import datetime
-import sys
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
SmallInteger
-from sqlalchemy.orm import relationship, backref
+from sqlalchemy.orm import relationship, backref, with_polymorphic
from sqlalchemy.orm.collections import attribute_mapped_collection
from sqlalchemy.sql.expression import desc
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.util import memoized_property
-from mediagoblin.db.sql.extratypes import PathTupleWithSlashes, JSONEncoded
-from mediagoblin.db.sql.base import Base, DictReadAttrProxy
-from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
-from mediagoblin.db.sql.base import Session
+
+from mediagoblin.db.extratypes import PathTupleWithSlashes, JSONEncoded
+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
# this import-based meddling...
from migrate import changeset
+_log = logging.getLogger(__name__)
+
class User(Base, UserMixin):
"""
id = Column(Integer, primary_key=True)
username = Column(Unicode, nullable=False, unique=True)
+ # Note: no db uniqueness constraint on email because it's not
+ # reliable (many email systems case insensitive despite against
+ # the RFC) and because it would be a mess to implement at this
+ # point.
email = Column(Unicode, nullable=False)
- created = Column(DateTime, nullable=False, default=datetime.datetime.now)
- pw_hash = Column(Unicode, nullable=False)
+ pw_hash = Column(Unicode)
email_verified = Column(Boolean, default=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
status = Column(Unicode, default=u"needs_email_verification", nullable=False)
# Intented to be nullable=False, but migrations would not work for it
# set to nullable=True implicitly.
wants_comment_notification = Column(Boolean, default=True)
- verification_key = Column(Unicode)
+ license_preference = Column(Unicode)
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
- fp_verification_key = Column(Unicode)
- fp_token_expire = Column(DateTime)
## TODO
# plugin data would be in a separate model
'admin' if self.is_admin else 'user',
self.username)
+ def delete(self, **kwargs):
+ """Deletes a User and all related entries/comments/files/..."""
+ # Collections get deleted by relationships.
+
+ media_entries = MediaEntry.query.filter(MediaEntry.uploader == self.id)
+ for media in media_entries:
+ # TODO: Make sure that "MediaEntry.delete()" also deletes
+ # all related files/Comments
+ media.delete(del_orphan_tags=False, commit=False)
+
+ # Delete now unused tags
+ # TODO: import here due to cyclic imports!!! This cries for refactoring
+ from mediagoblin.db.util import clean_orphan_tags
+ clean_orphan_tags(commit=False)
+
+ # Delete user, pass through commit=False/True in kwargs
+ super(User, self).delete(**kwargs)
+ _log.info('Deleted user "{0}" account'.format(self.username))
+
class MediaEntry(Base, MediaEntryMixin):
"""
)
tags_helper = relationship("MediaTag",
- cascade="all, delete-orphan"
+ cascade="all, delete-orphan" # should be automatically deleted
)
tags = association_proxy("tags_helper", "dict_view",
creator=lambda v: MediaTag(name=v["name"], slug=v["slug"])
collections = association_proxy("collections_helper", "in_collection")
## TODO
- # media_data
# fail_error
def get_comments(self, ascending=False):
order_col = MediaComment.created
if not ascending:
order_col = desc(order_col)
- return MediaComment.query.filter_by(
- media_entry=self.id).order_by(order_col)
+ return self.all_comments.order_by(order_col)
def url_to_prev(self, urlgen):
"""get the next 'newer' entry by this user"""
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')
id=self.id,
title=safe_title)
+ def delete(self, del_orphan_tags=True, **kwargs):
+ """Delete MediaEntry and all related files/attachments/comments
+
+ This will *not* automatically delete unused collections, which
+ can remain empty...
+
+ :param del_orphan_tags: True/false if we delete unused Tags too
+ :param commit: True/False if this should end the db transaction"""
+ # User's CollectionItems are automatically deleted via "cascade".
+ # Comments on this Media are deleted by cascade, hopefully.
+
+ # Delete all related files/attachments
+ try:
+ delete_media_files(self)
+ except OSError, error:
+ # Returns list of files we failed to delete
+ _log.error('No such files from the user "{1}" to delete: '
+ '{0}'.format(str(error), self.get_uploader))
+ _log.info('Deleted Media entry id "{0}"'.format(self.id))
+ # Related MediaTag's are automatically cleaned, but we might
+ # want to clean out unused Tag's too.
+ if del_orphan_tags:
+ # TODO: Import here due to cyclic imports!!!
+ # This cries for refactoring
+ from mediagoblin.db.util import clean_orphan_tags
+ clean_orphan_tags(commit=False)
+ # pass through commit=False/True in kwargs
+ super(MediaEntry, self).delete(**kwargs)
+
class FileKeynames(Base):
"""
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"))
+ get_entry = relationship(MediaEntry,
+ backref=backref("comments",
+ lazy="dynamic",
+ cascade="all, delete-orphan"))
+
+ # Cascade: Comments are somewhat owned by their MediaEntry.
+ # So do the full thing.
+ # lazy=dynamic: MediaEntries might have many comments,
+ # so make the "all_comments" a query-like thing.
+ get_media_entry = relationship(MediaEntry,
+ backref=backref("all_comments",
+ lazy="dynamic",
+ cascade="all, delete-orphan"))
class Collection(Base, CollectionMixin):
+ """An 'album' or 'set' of media by a user.
+
+ On deletion, contained CollectionItems get automatically reaped via
+ SQL cascade"""
__tablename__ = "core__collections"
id = Column(Integer, primary_key=True)
title = Column(Unicode, nullable=False)
slug = Column(Unicode)
created = Column(DateTime, nullable=False, default=datetime.datetime.now,
- index=True)
+ index=True)
description = Column(UnicodeText)
creator = Column(Integer, ForeignKey(User.id), nullable=False)
+ # TODO: No of items in Collection. Badly named, can we migrate to num_items?
items = Column(Integer, default=0)
- get_creator = relationship(User)
+ # Cascade: Collections are owned by their creator. So do the full thing.
+ get_creator = relationship(User,
+ backref=backref("collections",
+ cascade="all, delete-orphan"))
+
+ __table_args__ = (
+ UniqueConstraint('creator', 'slug'),
+ {})
def get_collection_items(self, ascending=False):
+ #TODO, is this still needed with self.collection_items being available?
order_col = CollectionItem.position
if not ascending:
order_col = desc(order_col)
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"))
get_media_entry = relationship(MediaEntry)
return DictReadAttrProxy(self)
+class CommentSubscription(Base):
+ __tablename__ = 'core__comment_subscriptions'
+ id = Column(Integer, primary_key=True)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ media_entry_id = Column(Integer, ForeignKey(MediaEntry.id), nullable=False)
+ media_entry = relationship(MediaEntry,
+ backref=backref('comment_subscriptions',
+ cascade='all, delete-orphan'))
+
+ user_id = Column(Integer, ForeignKey(User.id), nullable=False)
+ user = relationship(User,
+ backref=backref('comment_subscriptions',
+ cascade='all, delete-orphan'))
+
+ notify = Column(Boolean, nullable=False, default=True)
+ send_email = Column(Boolean, nullable=False, default=True)
+
+ def __repr__(self):
+ return ('<{classname} #{id}: {user} {media} notify: '
+ '{notify} email: {email}>').format(
+ id=self.id,
+ classname=self.__class__.__name__,
+ user=self.user,
+ media=self.media_entry,
+ notify=self.notify,
+ email=self.send_email)
+
+
+class Notification(Base):
+ __tablename__ = 'core__notifications'
+ id = Column(Integer, primary_key=True)
+ type = Column(Unicode)
+
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+ user_id = Column(Integer, ForeignKey('core__users.id'), nullable=False,
+ index=True)
+ seen = Column(Boolean, default=lambda: False, index=True)
+ user = relationship(
+ User,
+ backref=backref('notifications', cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'notification',
+ 'polymorphic_on': type
+ }
+
+ def __repr__(self):
+ return '<{klass} #{id}: {user}: {subject} ({seen})>'.format(
+ id=self.id,
+ klass=self.__class__.__name__,
+ user=self.user,
+ subject=getattr(self, 'subject', None),
+ seen='unseen' if not self.seen else 'seen')
+
+
+class CommentNotification(Notification):
+ __tablename__ = 'core__comment_notifications'
+ id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaComment.id))
+ subject = relationship(
+ MediaComment,
+ backref=backref('comment_notifications', cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'comment_notification'
+ }
+
+
+class ProcessingNotification(Notification):
+ __tablename__ = 'core__processing_notifications'
+
+ id = Column(Integer, ForeignKey(Notification.id), primary_key=True)
+
+ subject_id = Column(Integer, ForeignKey(MediaEntry.id))
+ subject = relationship(
+ MediaEntry,
+ backref=backref('processing_notifications',
+ cascade='all, delete-orphan'))
+
+ __mapper_args__ = {
+ 'polymorphic_identity': 'processing_notification'
+ }
+
+
+with_polymorphic(
+ Notification,
+ [ProcessingNotification, CommentNotification])
+
MODELS = [
- User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
- MediaAttachmentFile, ProcessingMetaData]
+ User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
+ MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
+ Notification, CommentNotification, ProcessingNotification,
+ CommentSubscription]
######################################################