-import datetime, uuid
-
-from mongokit import Document, Set
-
-from mediagoblin import util
-from mediagoblin.auth import lib as auth_lib
-from mediagoblin import mg_globals
-from mediagoblin.db import migrations
-from mediagoblin.db.util import ASCENDING, DESCENDING, ObjectId
-
-###################
-# Custom validators
-###################
-
-########
-# Models
-########
-
-
-class User(Document):
- __collection__ = 'users'
-
- structure = {
- 'username': unicode,
- 'email': unicode,
- 'created': datetime.datetime,
- 'plugin_data': dict, # plugins can dump stuff here.
- 'pw_hash': unicode,
- 'email_verified': bool,
- 'status': unicode,
- 'verification_key': unicode,
- 'is_admin': bool,
- 'url' : unicode,
- 'bio' : unicode
- }
-
- required_fields = ['username', 'created', 'pw_hash', 'email']
-
- default_values = {
- 'created': datetime.datetime.utcnow,
- 'email_verified': False,
- 'status': u'needs_email_verification',
- 'verification_key': lambda: unicode(uuid.uuid4()),
- 'is_admin': False}
-
- migration_handler = migrations.UserMigration
-
- def check_login(self, password):
- """
- See if a user can login with this password
- """
- return auth_lib.bcrypt_check_password(
- password, self['pw_hash'])
+"""
+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.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.extratypes import PathTupleWithSlashes, JSONEncoded
+from mediagoblin.db.base import Base, DictReadAttrProxy, Session
+from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
+from mediagoblin.tools.files import delete_media_files
+
+# It's actually kind of annoying how sqlalchemy-migrate does this, if
+# I understand it right, but whatever. Anyway, don't remove this :P
+#
+# We could do migration calls more manually instead of relying on
+# this import-based meddling...
+from migrate import changeset
+
+_log = logging.getLogger(__name__)
+
+
+class User(Base, UserMixin):
+ """
+ TODO: We should consider moving some rarely used fields
+ into some sort of "shadow" table.
+ """
+ __tablename__ = "core__users"
+
+ id = Column(Integer, primary_key=True)
+ username = Column(Unicode, nullable=False, unique=True)
+ email = Column(Unicode, nullable=False)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ pw_hash = Column(Unicode, nullable=False)
+ email_verified = Column(Boolean, default=False)
+ 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)
+ 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
+
+ def __repr__(self):
+ return '<{0} #{1} {2} {3} "{4}">'.format(
+ self.__class__.__name__,
+ self.id,
+ 'verified' if self.email_verified else 'non-verified',
+ 'admin' if self.is_admin else 'user',
+ self.username)
+
+ 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)
+
+ 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):
+ """
+ TODO: Consider fetching the media_files using join
+ """
+ __tablename__ = "core__media_entries"
+
+ id = Column(Integer, primary_key=True)
+ uploader = Column(Integer, ForeignKey(User.id), nullable=False, index=True)
+ title = Column(Unicode, nullable=False)
+ slug = Column(Unicode)
+ created = Column(DateTime, nullable=False, default=datetime.datetime.now,
+ index=True)
+ description = Column(UnicodeText) # ??
+ media_type = Column(Unicode, nullable=False)
+ state = Column(Unicode, default=u'unprocessed', nullable=False)
+ # or use sqlalchemy.types.Enum?
+ license = Column(Unicode)
+ collected = Column(Integer, default=0)
+
+ fail_error = Column(Unicode)
+ fail_metadata = Column(JSONEncoded)
+
+ transcoding_progress = Column(SmallInteger)
+
+ queued_media_file = Column(PathTupleWithSlashes)
+
+ queued_task_id = Column(Unicode)
+
+ __table_args__ = (
+ UniqueConstraint('uploader', 'slug'),
+ {})
+
+ get_uploader = relationship(User)
+
+ media_files_helper = relationship("MediaFile",
+ collection_class=attribute_mapped_collection("name"),
+ cascade="all, delete-orphan"
+ )
+ media_files = association_proxy('media_files_helper', 'file_path',
+ creator=lambda k, v: MediaFile(name=k, file_path=v)
+ )
+
+ attachment_files_helper = relationship("MediaAttachmentFile",
+ order_by="MediaAttachmentFile.created"
+ )
+ attachment_files = association_proxy("attachment_files_helper", "dict_view",
+ creator=lambda v: MediaAttachmentFile(
+ name=v["name"], filepath=v["filepath"])
+ )
+
+ tags_helper = relationship("MediaTag",
+ 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_helper = relationship("CollectionItem",
+ cascade="all, delete-orphan"
+ )
+ 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)
+
+ def url_to_prev(self, urlgen):
+ """get the next 'newer' entry by this user"""
+ media = MediaEntry.query.filter(
+ (MediaEntry.uploader == self.uploader)
+ & (MediaEntry.state == u'processed')
+ & (MediaEntry.id > self.id)).order_by(MediaEntry.id).first()