Support Unicode characters in configuration values
[mediagoblin.git] / mediagoblin / db / base.py
index c0cefdc27024d2c9ba44c957f00c8baafc0c65ec..c59b0ebf732d5b8be67c8ff8cce44300239ddb4d 100644 (file)
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
+import six
+import copy
 
 from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import scoped_session, sessionmaker, object_session
+from sqlalchemy import inspect
+
+from mediagoblin.tools.transition import DISABLE_GLOBALS
+
+if not DISABLE_GLOBALS:
+    from sqlalchemy.orm import scoped_session, sessionmaker
+    Session = scoped_session(sessionmaker())
+
+class FakeCursor(object):
 
-Session = scoped_session(sessionmaker())
+    def __init__ (self, cursor, mapper, filter=None):
+        self.cursor = cursor
+        self.mapper = mapper
+        self.filter = filter
 
+    def count(self):
+        return self.cursor.count()
+
+    def __copy__(self):
+        # Or whatever the function is named to make
+        # copy.copy happy?
+        return FakeCursor(copy.copy(self.cursor), self.mapper, self.filter)
+
+    def __iter__(self):
+        return six.moves.filter(self.filter, six.moves.map(self.mapper, self.cursor))
+
+    def __getitem__(self, key):
+        return self.mapper(self.cursor[key])
+
+    def slice(self, *args, **kwargs):
+        r = self.cursor.slice(*args, **kwargs)
+        return list(six.moves.filter(self.filter, six.moves.map(self.mapper, r)))
 
 class GMGTableBase(object):
-    query = Session.query_property()
+    # Deletion types
+    HARD_DELETE = "hard-deletion"
+    SOFT_DELETE = "soft-deletion"
+
+    deletion_mode = HARD_DELETE
+
+    @property
+    def _session(self):
+        return inspect(self).session
+
+    @property
+    def _app(self):
+        return self._session.bind.app
+
+    if not DISABLE_GLOBALS:
+        query = Session.query_property()
 
     def get(self, key):
         return getattr(self, key)
@@ -31,16 +75,116 @@ class GMGTableBase(object):
         # The key *has* to exist on sql.
         return getattr(self, key)
 
-    def save(self):
-        sess = object_session(self)
-        if sess is None:
+    def save(self, commit=True):
+        sess = self._session
+        if sess is None and not DISABLE_GLOBALS:
             sess = Session()
+        assert sess is not None, "Can't save, %r has a detached session" % self
         sess.add(self)
-        sess.commit()
+        if commit:
+            sess.commit()
+        else:
+            sess.flush()
+
+    def delete(self, commit=True, deletion=None):
+        """ Delete the object either using soft or hard deletion """
+        # Get the setting in the model args if none has been specified.
+        if deletion is None:
+            deletion = self.deletion_mode
+
+        # If the item is in any collection then it should be removed, this will
+        # cause issues if it isn't. See #5382.
+        # Import here to prevent cyclic imports.
+        from mediagoblin.db.models import CollectionItem, GenericModelReference, \
+                                          Report, Notification, Comment
+        
+        # Some of the models don't have an "id" field which means they can't be
+        # used with GMR, these models won't be in collections because they
+        # can't be. We can skip all of this.
+        if hasattr(self, "id"):
+            # First find the GenericModelReference for this object
+            gmr = GenericModelReference.query.filter_by(
+                obj_pk=self.id,
+                model_type=self.__tablename__
+            ).first()
+
+            # If there is no gmr then we've got lucky, a GMR is a requirement of
+            # being in a collection.
+            if gmr is not None:
+                # Delete collections found
+                items = CollectionItem.query.filter_by(
+                    object_id=gmr.id
+                )
+                items.delete()
+
+                # Delete notifications found
+                notifications = Notification.query.filter_by(
+                    object_id=gmr.id
+                )
+                notifications.delete()
+                
+                # Delete this as a comment
+                comments = Comment.query.filter_by(
+                    comment_id=gmr.id
+                )
+                comments.delete()
+
+                # Set None on reports found
+                reports = Report.query.filter_by(
+                    object_id=gmr.id
+                )
+                for report in reports:
+                    report.object_id = None
+                    report.save(commit=commit)
+
+        # Hand off to the correct deletion function.
+        if deletion == self.HARD_DELETE:
+            return self.hard_delete(commit=commit)
+        elif deletion == self.SOFT_DELETE:
+            return self.soft_delete(commit=commit)
+        else:
+            raise ValueError(
+                "Invalid deletion mode {mode!r}".format(
+                    mode=deletion
+                )
+            )
+
+    def soft_delete(self, commit):
+        # Create the graveyard version of this model
+        # Importing this here due to cyclic imports
+        from mediagoblin.db.models import User, Graveyard, GenericModelReference
+        
+        tombstone = Graveyard()
+        if getattr(self, "public_id", None) is not None:
+            tombstone.public_id = self.public_id
+
+        # This is a special case, we don't want to save any actor if the thing
+        # being soft deleted is a User model as this would create circular
+        # ForeignKeys
+        if not isinstance(self, User):
+            tombstone.actor = User.query.filter_by(
+                id=self.actor
+            ).first()
+        tombstone.object_type = self.object_type
+        tombstone.save(commit=False)
+
+        # There will be a lot of places where the GenericForeignKey will point
+        # to the model, we want to remap those to our tombstone.
+        gmrs = GenericModelReference.query.filter_by(
+            obj_pk=self.id,
+            model_type=self.__tablename__
+        ).update({
+            "obj_pk": tombstone.id,
+            "model_type": tombstone.__tablename__,
+        })
+
+        
+        # Now we can go ahead and actually delete the model.
+        return self.hard_delete(commit=commit)
 
-    def delete(self, commit=True):
+    def hard_delete(self, commit):
         """Delete the object and commit the change immediately by default"""
-        sess = object_session(self)
+        sess = self._session
         assert sess is not None, "Not going to delete detached %r" % self
         sess.delete(self)
         if commit: