Merge remote-tracking branch 'refs/remotes/rodney757/reprocessing'
[mediagoblin.git] / mediagoblin / db / models.py
index f5470cb96c330122158007b22f3505f84c83f0a6..809e47220dd6b6901b2361b8d563ca045ec7caac 100644 (file)
@@ -24,15 +24,17 @@ import datetime
 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.extratypes import PathTupleWithSlashes, JSONEncoded
 from mediagoblin.db.base import Base, DictReadAttrProxy
-from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, MediaCommentMixin, CollectionMixin, CollectionItemMixin
+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
 
@@ -60,17 +62,20 @@ class User(Base, UserMixin):
     # 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)
+    wants_notifications = Column(Boolean, default=True)
     license_preference = Column(Unicode)
     is_admin = Column(Boolean, default=False, nullable=False)
     url = Column(Unicode)
     bio = Column(UnicodeText)  # ??
+    uploaded = Column(Integer, default=0)
+    upload_limit = Column(Integer)
 
     ## TODO
     # plugin data would be in a separate model
@@ -103,6 +108,72 @@ class User(Base, UserMixin):
         _log.info('Deleted user "{0}" account'.format(self.username))
 
 
+class Client(Base):
+    """
+        Model representing a client - Used for API Auth
+    """
+    __tablename__ = "core__clients"
+
+    id = Column(Unicode, nullable=True, primary_key=True)
+    secret = Column(Unicode, nullable=False)
+    expirey = Column(DateTime, nullable=True)
+    application_type = Column(Unicode, nullable=False)
+    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+    # optional stuff
+    redirect_uri = Column(JSONEncoded, nullable=True)
+    logo_url = Column(Unicode, nullable=True)
+    application_name = Column(Unicode, nullable=True)
+    contacts = Column(JSONEncoded, nullable=True)
+
+    def __repr__(self):
+        if self.application_name:
+            return "<Client {0} - {1}>".format(self.application_name, self.id)
+        else:
+            return "<Client {0}>".format(self.id)
+
+class RequestToken(Base):
+    """
+        Model for representing the request tokens
+    """
+    __tablename__ = "core__request_tokens"
+
+    token = Column(Unicode, primary_key=True)
+    secret = Column(Unicode, nullable=False)
+    client = Column(Unicode, ForeignKey(Client.id))
+    user = Column(Integer, ForeignKey(User.id), nullable=True)
+    used = Column(Boolean, default=False)
+    authenticated = Column(Boolean, default=False)
+    verifier = Column(Unicode, nullable=True)
+    callback = Column(Unicode, nullable=False, default=u"oob")
+    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+class AccessToken(Base):
+    """
+        Model for representing the access tokens
+    """
+    __tablename__ = "core__access_tokens"
+
+    token = Column(Unicode, nullable=False, primary_key=True)
+    secret = Column(Unicode, nullable=False)
+    user = Column(Integer, ForeignKey(User.id))
+    request_token = Column(Unicode, ForeignKey(RequestToken.token))
+    created = Column(DateTime, nullable=False, default=datetime.datetime.now)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+
+
+class NonceTimestamp(Base):
+    """
+        A place the timestamp and nonce can be stored - this is for OAuth1
+    """
+    __tablename__ = "core__nonce_timestamps"
+
+    nonce = Column(Unicode, nullable=False, primary_key=True)
+    timestamp = Column(DateTime, nullable=False, primary_key=True)
+
+
 class MediaEntry(Base, MediaEntryMixin):
     """
     TODO: Consider fetching the media_files using join
@@ -121,6 +192,7 @@ class MediaEntry(Base, MediaEntryMixin):
         # or use sqlalchemy.types.Enum?
     license = Column(Unicode)
     collected = Column(Integer, default=0)
+    file_size = Column(Integer, default=0)
 
     fail_error = Column(Unicode)
     fail_metadata = Column(JSONEncoded)
@@ -195,6 +267,34 @@ class MediaEntry(Base, MediaEntryMixin):
         if media is not None:
             return media.url_for_self(urlgen)
 
+    def get_file_metadata(self, file_key, metadata_key=None):
+        """
+        Return the file_metadata dict of a MediaFile. If metadata_key is given,
+        return the value of the key.
+        """
+        media_file = MediaFile.query.filter_by(media_entry=self.id,
+                                               name=unicode(file_key)).first()
+
+        if media_file:
+            if metadata_key:
+                return media_file.file_metadata.get(metadata_key, None)
+
+            return media_file.file_metadata
+
+    def set_file_metadata(self, file_key, **kwargs):
+        """
+        Update the file_metadata of a MediaFile.
+        """
+        media_file = MediaFile.query.filter_by(media_entry=self.id,
+                                               name=unicode(file_key)).first()
+
+        file_metadata = media_file.file_metadata or {}
+
+        for key, value in kwargs.iteritems():
+            file_metadata[key] = value
+
+        media_file.file_metadata = file_metadata
+
     @property
     def media_data(self):
         return getattr(self, self.media_data_ref)
@@ -291,6 +391,7 @@ class MediaFile(Base):
         nullable=False)
     name_id = Column(SmallInteger, ForeignKey(FileKeynames.id), nullable=False)
     file_path = Column(PathTupleWithSlashes)
+    file_metadata = Column(JSONEncoded)
 
     __table_args__ = (
         PrimaryKeyConstraint('media_entry', 'name_id'),
@@ -389,6 +490,10 @@ class MediaComment(Base, MediaCommentMixin):
                               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.
@@ -481,10 +586,119 @@ class ProcessingMetaData(Base):
         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, Client, RequestToken, AccessToken, NonceTimestamp, MediaEntry, Tag,
+    MediaTag, MediaComment, Collection, CollectionItem, MediaFile, FileKeynames,
+    MediaAttachmentFile, ProcessingMetaData, Notification, CommentNotification,
+    ProcessingNotification, CommentSubscription]
 
+"""
+ Foundations are the default rows that are created immediately after the tables
+ are initialized. Each entry to  this dictionary should be in the format of:
+                 ModelConstructorObject:List of Dictionaries
+ (Each Dictionary represents a row on the Table to be created, containing each
+  of the columns' names as a key string, and each of the columns' values as a
+  value)
+
+ ex. [NOTE THIS IS NOT BASED OFF OF OUR USER TABLE]
+    user_foundations = [{'name':u'Joanna', 'age':24},
+                        {'name':u'Andrea', 'age':41}]
+
+    FOUNDATIONS = {User:user_foundations}
+"""
+FOUNDATIONS = {}
 
 ######################################################
 # Special, migrations-tracking table