Add LocalUser and RemoteUser and migration
authorJessica Tallon <tsyesika@tsyesika.se>
Fri, 26 Jun 2015 13:29:44 +0000 (15:29 +0200)
committerJessica Tallon <tsyesika@tsyesika.se>
Fri, 31 Jul 2015 13:14:41 +0000 (15:14 +0200)
mediagoblin/db/migrations.py
mediagoblin/db/models.py

index 4f2f8915352478dba6d26fe244c4ec393839af0b..02dc996c42fb5a4021a5da98bd5d97d67b7a039b 100644 (file)
@@ -1429,3 +1429,123 @@ def remove_activityintermediator(db):
 
     # Commit the changes
     db.commit()
+
+##
+# Migrations for converting the User model into a Local and Remote User
+# setup.
+##
+
+class LocalUser_V0(declarative_base()):
+    __tablename__ = "core__local_users"
+
+    id = Column(Integer, ForeignKey(User.id), primary_key=True)
+    username = Column(Unicode, nullable=False, unique=True)
+    email = Column(Unicode, nullable=False)
+    pw_hash = Column(Unicode)
+
+    wants_comment_notification = Column(Boolean, default=True)
+    wants_notifications = Column(Boolean, default=True)
+    license_preference = Column(Unicode)
+    uploaded = Column(Integer, default=0)
+    upload_limit = Column(Integer)
+
+class RemoteUser_V0(declarative_base()):
+    __tablename__ = "core__remote_users"
+
+    id = Column(Integer, ForeignKey(User.id), primary_key=True)
+    webfinger = Column(Unicode, unique=True)
+
+@RegisterMigration(32, MIGRATIONS)
+def federation_user_create_tables(db):
+    """
+    Create all the tables
+    """
+    # Create tables needed
+    LocalUser_V0.__table__.create(db.bind)
+    RemoteUser_V0.__table__.create(db.bind)
+    db.commit()
+
+    # Create the fields
+    metadata = MetaData(bind=db.bind)
+    user_table = inspect_table(metadata, "core__users")
+
+    updated_column = Column(
+        "updated",
+        DateTime,
+        default=datetime.datetime.utcnow
+    )
+    updated_column.create(user_table)
+
+    name_column = Column(
+        "name",
+        Unicode
+    )
+    name_column.create(user_table)
+
+    db.commit()
+
+@RegisterMigration(33, MIGRATIONS)
+def federation_user_migrate_data(db):
+    """
+    Migrate the data over to the new user models
+    """
+    metadata = MetaData(bind=db.bind)
+
+    user_table = inspect_table(metadata, "core__users")
+    local_user_table = inspect_table(metadata, "core__local_users")
+
+    for user in db.execute(user_table.select()):
+        db.execute(local_user_table.insert().values(
+            id=user.id,
+            username=user.username,
+            email=user.email,
+            pw_hash=user.pw_hash,
+            wants_comment_notification=user.wants_comment_notification,
+            wants_notifications=user.wants_notifications,
+            license_preference=user.license_preference,
+            uploaded=user.uploaded,
+            upload_limit=user.upload_limit
+        ))
+
+        db.execute(user_table.update().where(user_table.c.id==user.id).values(
+            updated=user.created
+        ))
+
+    db.commit()
+
+@RegisterMigration(34, MIGRATIONS)
+def federation_remove_fields(db):
+    """
+    This removes the fields from User model which aren't shared
+    """
+    metadata = MetaData(bind=db.bind)
+
+    user_table = inspect_table(metadata, "core__users")
+
+    # Remove the columns moved to LocalUser from User
+    username_column = user_table.columns["username"]
+    username_column.drop()
+
+    email_column = user_table.columns["email"]
+    email_column.drop()
+
+    pw_hash_column = user_table.columns["pw_hash"]
+    pw_hash_column.drop()
+
+    wcn_column = user_table.columns["wants_comment_notification"]
+    wcn_column.drop()
+
+    wants_notifications_column = user_table.columns["wants_notifications"]
+    wants_notifications_column.drop()
+
+    license_preference_column = user_table.columns["license_preference"]
+    license_preference_column.drop()
+
+    uploaded_column = user_table.columns["uploaded"]
+    uploaded_column.drop()
+
+    upload_limit_column = user_table.columns["upload_limit"]
+    upload_limit_column.drop()
+
+    db.commit()
+
index 054e1677716c6b867d4da126988a644e79101eb4..ef37aef8fc6d3ba843ab418abde438942d64f5b9 100644 (file)
@@ -222,12 +222,92 @@ class Location(Base):
 
 class User(Base, UserMixin):
     """
-    TODO: We should consider moving some rarely used fields
-    into some sort of "shadow" table.
+    Base user that is common amongst LocalUser and RemoteUser.
+
+    This holds all the fields which are common between both the Local and Remote
+    user models.
+
+    NB: ForeignKeys should reference this User model and NOT the LocalUser or
+        RemoteUser models.
     """
     __tablename__ = "core__users"
 
     id = Column(Integer, primary_key=True)
+    url = Column(Unicode)
+    bio = Column(UnicodeText)
+    name = Column(Unicode)
+
+    created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
+    updated = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
+
+    location = Column(Integer, ForeignKey("core__locations.id"))
+
+    # Lazy getters
+    get_location = relationship("Location", lazy="joined")
+
+    def has_privilege(self, privilege, allow_admin=True):
+        """
+        This method checks to make sure a user has all the correct privileges
+        to access a piece of content.
+
+        :param  privilege       A unicode object which represent the different
+                                privileges which may give the user access to
+                                content.
+
+        :param  allow_admin     If this is set to True the then if the user is
+                                an admin, then this will always return True
+                                even if the user hasn't been given the
+                                privilege. (defaults to True)
+        """
+        priv = Privilege.query.filter_by(privilege_name=privilege).one()
+        if priv in self.all_privileges:
+            return True
+        elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
+            return True
+
+        return False
+
+    def is_banned(self):
+        """
+        Checks if this user is banned.
+
+            :returns                True if self is banned
+            :returns                False if self is not
+        """
+        return UserBan.query.get(self.id) is not None
+
+    def serialize(self, request):
+        published = UTC.localize(self.created)
+        updated = UTC.localize(self.updated)
+        user = {
+            "published": published.isoformat(),
+            "updated": updated.isoformat(),
+            "objectType": self.object_type,
+            "pump_io": {
+                "shared": False,
+                "followed": False,
+            },
+        }
+
+        if self.bio:
+            user.update({"summary": self.bio})
+        if self.url:
+            user.update({"url": self.url})
+        if self.location:
+            user.update({"location": self.get_location.serialize(request)})
+
+    def unserialize(self, data):
+        if "summary" in data:
+            self.bio = data["summary"]
+
+        if "location" in data:
+            Location.create(data, self)
+
+class LocalUser(User):
+    """ This represents a user registered on this instance """
+    __tablename__ = "core__local_users"
+
+    id = Column(Integer, ForeignKey("core__users.id"), 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
@@ -235,18 +315,14 @@ class User(Base, UserMixin):
     # point.
     email = Column(Unicode, nullable=False)
     pw_hash = Column(Unicode)
-    created = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
+
     # 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)
-    url = Column(Unicode)
-    bio = Column(UnicodeText)  # ??
     uploaded = Column(Integer, default=0)
     upload_limit = Column(Integer)
-    location = Column(Integer, ForeignKey("core__locations.id"))
-    get_location = relationship("Location", lazy="joined")
 
     ## TODO
     # plugin data would be in a separate model
@@ -278,50 +354,11 @@ class User(Base, UserMixin):
         super(User, self).delete(**kwargs)
         _log.info('Deleted user "{0}" account'.format(self.username))
 
-    def has_privilege(self, privilege, allow_admin=True):
-        """
-        This method checks to make sure a user has all the correct privileges
-        to access a piece of content.
-
-        :param  privilege       A unicode object which represent the different
-                                privileges which may give the user access to
-                                content.
-
-        :param  allow_admin     If this is set to True the then if the user is
-                                an admin, then this will always return True
-                                even if the user hasn't been given the
-                                privilege. (defaults to True)
-        """
-        priv = Privilege.query.filter_by(privilege_name=privilege).one()
-        if priv in self.all_privileges:
-            return True
-        elif allow_admin and self.has_privilege(u'admin', allow_admin=False):
-            return True
-
-        return False
-
-    def is_banned(self):
-        """
-        Checks if this user is banned.
-
-            :returns                True if self is banned
-            :returns                False if self is not
-        """
-        return UserBan.query.get(self.id) is not None
-
-
     def serialize(self, request):
-        published = UTC.localize(self.created)
         user = {
             "id": "acct:{0}@{1}".format(self.username, request.host),
-            "published": published.isoformat(),
             "preferredUsername": self.username,
             "displayName": "{0}@{1}".format(self.username, request.host),
-            "objectType": self.object_type,
-            "pump_io": {
-                "shared": False,
-                "followed": False,
-            },
             "links": {
                 "self": {
                     "href": request.urlgen(
@@ -347,21 +384,23 @@ class User(Base, UserMixin):
             },
         }
 
-        if self.bio:
-            user.update({"summary": self.bio})
-        if self.url:
-            user.update({"url": self.url})
-        if self.location:
-            user.update({"location": self.get_location.serialize(request)})
-
+        user.update(super(LocalUser, self).serialize(request))
         return user
 
-    def unserialize(self, data):
-        if "summary" in data:
-            self.bio = data["summary"]
+class RemoteUser(User):
+    """ User that is on another (remote) instance """
+    __tablename__ = "core__remote_users"
+
+    id = Column(Integer, ForeignKey("core__users.id"), primary_key=True)
+    webfinger = Column(Unicode, unique=True)
+
+    def __repr__(self):
+        return "<{0} #{1} {2}>".format(
+            self.__class__.__name__,
+            self.id,
+            self.webfinger
+        )
 
-        if "location" in data:
-            Location.create(data, self)
 
 class Client(Base):
     """