Merge branch 'location'
authorJessica Tallon <jessica@megworld.co.uk>
Thu, 9 Oct 2014 18:20:13 +0000 (19:20 +0100)
committerJessica Tallon <jessica@megworld.co.uk>
Thu, 9 Oct 2014 18:20:13 +0000 (19:20 +0100)
Add Location model which holds textual, geolocation coordiantes
or postal addresses. This migrates data off Image model metadata
onto the general Location model. It also adds the ability for location
to be set on MediaEntry, User, MediaComment and Collection models.

The geolocation plugin has been updated so that the location can be displayed
in more general places rather than explicitely on the MediaEntry view.
If GPS coordiantes are set for the User the profile page will also have the
OSM provided by the geolocation plugin.

1  2 
mediagoblin/db/migrations.py
mediagoblin/db/models.py
mediagoblin/edit/views.py
mediagoblin/federation/views.py
mediagoblin/media_types/image/processing.py
mediagoblin/tests/test_submission.py

index 31b8333e71f4a3e9d17477acd25f192ad65445ce,be509a75857fb18784b8889c4e1a328ca01c639e..0e0ee6bec76a663d602854269893c217a8ee4c6a
@@@ -895,194 -891,39 +895,231 @@@ def revert_username_index(db)
  
      db.commit()
  
 -@RegisterMigration(24, MIGRATIONS)
 +class Generator_R0(declarative_base()):
 +    __tablename__ = "core__generators"
 +    id = Column(Integer, primary_key=True)
 +    name = Column(Unicode, nullable=False)
 +    published = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    object_type = Column(Unicode, nullable=False)
 +
 +class ActivityIntermediator_R0(declarative_base()):
 +    __tablename__ = "core__activity_intermediators"
 +    id = Column(Integer, primary_key=True)
 +    type = Column(Unicode, nullable=False)
 +
 +class Activity_R0(declarative_base()):
 +    __tablename__ = "core__activities"
 +    id = Column(Integer, primary_key=True)
 +    actor = Column(Integer, ForeignKey(User.id), nullable=False)
 +    published = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    verb = Column(Unicode, nullable=False)
 +    content = Column(Unicode, nullable=True)
 +    title = Column(Unicode, nullable=True)
 +    generator = Column(Integer, ForeignKey(Generator_R0.id), nullable=True)
 +    object = Column(Integer,
 +                    ForeignKey(ActivityIntermediator_R0.id),
 +                    nullable=False)
 +    target = Column(Integer,
 +                    ForeignKey(ActivityIntermediator_R0.id),
 +                    nullable=True)
 +
 +@RegisterMigration(24, MIGRATIONS)
 +def activity_migration(db):
 +    """
 +    Creates everything to create activities in GMG
 +    - Adds Activity, ActivityIntermediator and Generator table
 +    - Creates GMG service generator for activities produced by the server
 +    - Adds the activity_as_object and activity_as_target to objects/targets
 +    - Retroactively adds activities for what we can acurately work out
 +    """
 +    # Set constants we'll use later
 +    FOREIGN_KEY = "core__activity_intermediators.id"
 +    ACTIVITY_COLUMN = "activity"
 +
 +    # Create the new tables.
 +    ActivityIntermediator_R0.__table__.create(db.bind)
 +    Generator_R0.__table__.create(db.bind)
 +    Activity_R0.__table__.create(db.bind)
 +    db.commit()
 +
 +    # Initiate the tables we want to use later
 +    metadata = MetaData(bind=db.bind)
 +    user_table = inspect_table(metadata, "core__users")
 +    activity_table = inspect_table(metadata, "core__activities")
 +    generator_table = inspect_table(metadata, "core__generators")
 +    collection_table = inspect_table(metadata, "core__collections")
 +    media_entry_table = inspect_table(metadata, "core__media_entries")
 +    media_comments_table = inspect_table(metadata, "core__media_comments")
 +    ai_table = inspect_table(metadata, "core__activity_intermediators")
 +
 +
 +    # Create the foundations for Generator
 +    db.execute(generator_table.insert().values(
 +        name="GNU Mediagoblin",
 +        object_type="service",
 +        published=datetime.datetime.now(),
 +        updated=datetime.datetime.now()
 +    ))
 +    db.commit()
 +
 +    # Get the ID of that generator
 +    gmg_generator = db.execute(generator_table.select(
 +        generator_table.c.name==u"GNU Mediagoblin")).first()
 +
 +
 +    # Now we want to modify the tables which MAY have an activity at some point
 +    media_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
 +    media_col.create(media_entry_table)
 +
 +    user_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
 +    user_col.create(user_table)
 +
 +    comments_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
 +    comments_col.create(media_comments_table)
 +
 +    collection_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
 +    collection_col.create(collection_table)
 +    db.commit()
 +
 +
 +    # Now we want to retroactively add what activities we can
 +    # first we'll add activities when people uploaded media.
 +    # these can't have content as it's not fesible to get the
 +    # correct content strings.
 +    for media in db.execute(media_entry_table.select()):
 +        # Now we want to create the intermedaitory
 +        db_ai = db.execute(ai_table.insert().values(
 +            type="media",
 +        ))
 +        db_ai = db.execute(ai_table.select(
 +            ai_table.c.id==db_ai.inserted_primary_key[0]
 +        )).first()
 +
 +        # Add the activity
 +        activity = {
 +            "verb": "create",
 +            "actor": media.uploader,
 +            "published": media.created,
 +            "updated": media.created,
 +            "generator": gmg_generator.id,
 +            "object": db_ai.id
 +        }
 +        db.execute(activity_table.insert().values(**activity))
 +
 +        # Add the AI to the media.
 +        db.execute(media_entry_table.update().values(
 +            activity=db_ai.id
 +        ).where(media_entry_table.c.id==media.id))
 +
 +    # Now we want to add all the comments people made
 +    for comment in db.execute(media_comments_table.select()):
 +        # Get the MediaEntry for the comment
 +        media_entry = db.execute(
 +            media_entry_table.select(
 +                media_entry_table.c.id==comment.media_entry
 +        )).first()
 +
 +        # Create an AI for target
 +        db_ai_media = db.execute(ai_table.select(
 +            ai_table.c.id==media_entry.activity
 +        )).first().id
 +
 +        db.execute(
 +            media_comments_table.update().values(
 +                activity=db_ai_media
 +        ).where(media_comments_table.c.id==media_entry.id))
 +
 +        # Now create the AI for the comment
 +        db_ai_comment = db.execute(ai_table.insert().values(
 +            type="comment"
 +        )).inserted_primary_key[0]
 +
 +        activity = {
 +            "verb": "comment",
 +            "actor": comment.author,
 +            "published": comment.created,
 +            "updated": comment.created,
 +            "generator": gmg_generator.id,
 +            "object": db_ai_comment,
 +            "target": db_ai_media,
 +        }
 +
 +        # Now add the comment object
 +        db.execute(activity_table.insert().values(**activity))
 +
 +        # Now add activity to comment
 +        db.execute(media_comments_table.update().values(
 +            activity=db_ai_comment
 +        ).where(media_comments_table.c.id==comment.id))
 +
 +    # Create 'create' activities for all collections
 +    for collection in db.execute(collection_table.select()):
 +        # create AI
 +        db_ai = db.execute(ai_table.insert().values(
 +            type="collection"
 +        ))
 +        db_ai = db.execute(ai_table.select(
 +            ai_table.c.id==db_ai.inserted_primary_key[0]
 +        )).first()
 +
 +        # Now add link the collection to the AI
 +        db.execute(collection_table.update().values(
 +            activity=db_ai.id
 +        ).where(collection_table.c.id==collection.id))
 +
 +        activity = {
 +            "verb": "create",
 +            "actor": collection.creator,
 +            "published": collection.created,
 +            "updated": collection.created,
 +            "generator": gmg_generator.id,
 +            "object": db_ai.id,
 +        }
 +
 +        db.execute(activity_table.insert().values(**activity))
 +
 +        # Now add the activity to the collection
 +        db.execute(collection_table.update().values(
 +            activity=db_ai.id
 +        ).where(collection_table.c.id==collection.id))
 +
 +    db.commit()
++
+ class Location_V0(declarative_base()):
+     __tablename__ = "core__locations"
+     id = Column(Integer, primary_key=True)
+     name = Column(Unicode)
+     position = Column(MutationDict.as_mutable(JSONEncoded))
+     address = Column(MutationDict.as_mutable(JSONEncoded))
++@RegisterMigration(25, MIGRATIONS)
+ def add_location_model(db):
+     """ Add location model """
+     metadata = MetaData(bind=db.bind)
+     # Create location table
+     Location_V0.__table__.create(db.bind)
+     db.commit()
+     # Inspect the tables we need
+     user = inspect_table(metadata, "core__users")
+     collections = inspect_table(metadata, "core__collections")
+     media_entry = inspect_table(metadata, "core__media_entries")
+     media_comments = inspect_table(metadata, "core__media_comments")
+     # Now add location support to the various models
+     col = Column("location", Integer, ForeignKey(Location_V0.id))
+     col.create(user)
+     col = Column("location", Integer, ForeignKey(Location_V0.id))
+     col.create(collections)
+     col = Column("location", Integer, ForeignKey(Location_V0.id))
+     col.create(media_entry)
+     col = Column("location", Integer, ForeignKey(Location_V0.id))
+     col.create(media_comments)
+     db.commit()
index 0069c85a7e34944dd1e5388ee22f0778d8e21358,20eccdaed4b48ac10eba26828cd7600d19ebed23..b1bdba887fa7c12af1f10e7a7a74cfb36533cc1b
@@@ -45,7 -48,81 +45,80 @@@ import si
  
  _log = logging.getLogger(__name__)
  
+ class Location(Base):
+     """ Represents a physical location """
+     __tablename__ = "core__locations"
+     id = Column(Integer, primary_key=True)
+     name = Column(Unicode)
+     # GPS coordinates
+     position = Column(MutationDict.as_mutable(JSONEncoded))
+     address = Column(MutationDict.as_mutable(JSONEncoded))
+     @classmethod
+     def create(cls, data, obj):
+         location = cls()
+         location.unserialize(data)
+         location.save()
+         obj.location = location.id
+         return location
+     def serialize(self, request):
+         location = {"objectType": "place"}
+         if self.name is not None:
+             location["name"] = self.name
+         if self.position:
+             location["position"] = self.position
+         if self.address:
+             location["address"] = self.address
+         return location
+     def unserialize(self, data):
+         if "name" in data:
+             self.name = data["name"]
+         self.position = {}
+         self.address = {}
+         # nicer way to do this?
+         if "position" in data:
+             # TODO: deal with ISO 9709 formatted string as position
+             if "altitude" in data["position"]:
+                 self.position["altitude"] = data["position"]["altitude"]
+             if "direction" in data["position"]:
+                 self.position["direction"] = data["position"]["direction"]
+             if "longitude" in data["position"]:
+                 self.position["longitude"] = data["position"]["longitude"]
+             if "latitude" in data["position"]:
+                 self.position["latitude"] = data["position"]["latitude"]
+         if "address" in data:
+             if "formatted" in data["address"]:
+                 self.address["formatted"] = data["address"]["formatted"]
+             if "streetAddress" in data["address"]:
+                 self.address["streetAddress"] = data["address"]["streetAddress"]
+             if "locality" in data["address"]:
+                 self.address["locality"] = data["address"]["locality"]
+             if "region" in data["address"]:
+                 self.address["region"] = data["address"]["region"]
+             if "postalCode" in data["address"]:
+                 self.address["postalCode"] = data["addresss"]["postalCode"]
+             if "country" in data["address"]:
+                 self.address["country"] = data["address"]["country"]
  
 -
  class User(Base, UserMixin):
      """
      TODO: We should consider moving some rarely used fields
      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")
  
 +    activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
 +
      ## TODO
      # plugin data would be in a separate model
  
@@@ -476,9 -563,11 +562,12 @@@ class MediaEntry(Base, MediaEntryMixin)
          if self.license:
              context["license"] = self.license
  
+         if self.location:
+             context["location"] = self.get_location.serialize(request)
          if show_comments:
 -            comments = [comment.serialize(request) for comment in self.get_comments()]
 +            comments = [
 +                comment.serialize(request) for comment in self.get_comments()]
              total = len(comments)
              context["replies"] = {
                  "totalItems": total,
@@@ -1074,183 -1157,19 +1177,188 @@@ class PrivilegeUserAssociation(Base)
          ForeignKey(Privilege.id),
          primary_key=True)
  
 +class Generator(Base):
 +    """ Information about what created an activity """
 +    __tablename__ = "core__generators"
 +
 +    id = Column(Integer, primary_key=True)
 +    name = Column(Unicode, nullable=False)
 +    published = Column(DateTime, default=datetime.datetime.now)
 +    updated = Column(DateTime, default=datetime.datetime.now)
 +    object_type = Column(Unicode, nullable=False)
 +
 +    def __repr__(self):
 +        return "<{klass} {name}>".format(
 +            klass=self.__class__.__name__,
 +            name=self.name
 +        )
 +
 +    def serialize(self, request):
 +        return {
 +            "id": self.id,
 +            "displayName": self.name,
 +            "published": self.published.isoformat(),
 +            "updated": self.updated.isoformat(),
 +            "objectType": self.object_type,
 +        }
 +
 +    def unserialize(self, data):
 +        if "displayName" in data:
 +            self.name = data["displayName"]
 +
 +
 +class ActivityIntermediator(Base):
 +    """
 +    This is used so that objects/targets can have a foreign key back to this
 +    object and activities can a foreign key to this object. This objects to be
 +    used multiple times for the activity object or target and also allows for
 +    different types of objects to be used as an Activity.
 +    """
 +    __tablename__ = "core__activity_intermediators"
 +
 +    id = Column(Integer, primary_key=True)
 +    type = Column(Unicode, nullable=False)
 +
 +    TYPES = {
 +        "user": User,
 +        "media": MediaEntry,
 +        "comment": MediaComment,
 +        "collection": Collection,
 +    }
 +
 +    def _find_model(self, obj):
 +        """ Finds the model for a given object """
 +        for key, model in self.TYPES.items():
 +            if isinstance(obj, model):
 +                return key, model
 +
 +        return None, None
 +
 +    def set(self, obj):
 +        """ This sets itself as the activity """
 +        key, model = self._find_model(obj)
 +        if key is None:
 +            raise ValueError("Invalid type of object given")
 +
 +        # We need to save so that self.id is populated
 +        self.type = key
 +        self.save()
 +
 +        # First set self as activity
 +        obj.activity = self.id
 +        obj.save()
 +
 +    def get(self):
 +        """ Finds the object for an activity """
 +        if self.type is None:
 +            return None
 +
 +        model = self.TYPES[self.type]
 +        return model.query.filter_by(activity=self.id).first()
 +
 +    def save(self, *args, **kwargs):
 +        if self.type not in self.TYPES.keys():
 +            raise ValueError("Invalid type set")
 +        Base.save(self, *args, **kwargs)
 +
 +class Activity(Base, ActivityMixin):
 +    """
 +    This holds all the metadata about an activity such as uploading an image,
 +    posting a comment, etc.
 +    """
 +    __tablename__ = "core__activities"
 +
 +    id = Column(Integer, primary_key=True)
 +    actor = Column(Integer,
 +                   ForeignKey("core__users.id"),
 +                   nullable=False)
 +    published = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
 +    verb = Column(Unicode, nullable=False)
 +    content = Column(Unicode, nullable=True)
 +    title = Column(Unicode, nullable=True)
 +    generator = Column(Integer,
 +                       ForeignKey("core__generators.id"),
 +                       nullable=True)
 +    object = Column(Integer,
 +                    ForeignKey("core__activity_intermediators.id"),
 +                    nullable=False)
 +    target = Column(Integer,
 +                    ForeignKey("core__activity_intermediators.id"),
 +                    nullable=True)
 +
 +    get_actor = relationship(User,
 +        foreign_keys="Activity.actor", post_update=True)
 +    get_generator = relationship(Generator)
 +
 +    def __repr__(self):
 +        if self.content is None:
 +            return "<{klass} verb:{verb}>".format(
 +                klass=self.__class__.__name__,
 +                verb=self.verb
 +            )
 +        else:
 +            return "<{klass} {content}>".format(
 +                klass=self.__class__.__name__,
 +                content=self.content
 +            )
 +
 +    @property
 +    def get_object(self):
 +        if self.object is None:
 +            return None
 +
 +        ai = ActivityIntermediator.query.filter_by(id=self.object).first()
 +        return ai.get()
 +
 +    def set_object(self, obj):
 +        self.object = self._set_model(obj)
 +
 +    @property
 +    def get_target(self):
 +        if self.target is None:
 +            return None
 +
 +        ai = ActivityIntermediator.query.filter_by(id=self.target).first()
 +        return ai.get()
 +
 +    def set_target(self, obj):
 +        self.target = self._set_model(obj)
 +
 +    def _set_model(self, obj):
 +        # Firstly can we set obj
 +        if not hasattr(obj, "activity"):
 +            raise ValueError(
 +                "{0!r} is unable to be set on activity".format(obj))
 +
 +        if obj.activity is None:
 +            # We need to create a new AI
 +            ai = ActivityIntermediator()
 +            ai.set(obj)
 +            ai.save()
 +            return ai.id
 +
 +        # Okay we should have an existing AI
 +        return ActivityIntermediator.query.filter_by(id=obj.activity).first().id
 +
 +    def save(self, set_updated=True, *args, **kwargs):
 +        if set_updated:
 +            self.updated = datetime.datetime.now()
 +        super(Activity, self).save(*args, **kwargs)
  
+ with_polymorphic(
+     Notification,
+     [ProcessingNotification, CommentNotification])
  MODELS = [
      User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
      MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
      Notification, CommentNotification, ProcessingNotification, Client,
      CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
 -    Privilege, PrivilegeUserAssociation,
 +      Privilege, PrivilegeUserAssociation,
      RequestToken, AccessToken, NonceTimestamp,
-     Activity, ActivityIntermediator, Generator]
++    Activity, ActivityIntermediator, Generator,
+     Location]
  
  """
   Foundations are the default rows that are created immediately after the tables
index 2ccf11aebcb8c1cbc7a125b9821c99d48df1da82,17c83d465574b24b7fc252c2b1668b347ddc0f3b..30a32a7e36fa6701e19e3ffb939f43c50c78e67e
@@@ -47,7 -45,7 +47,7 @@@ from mediagoblin.tools.text import 
      convert_to_tag_list_of_dicts, media_tags_as_string)
  from mediagoblin.tools.url import slugify
  from mediagoblin.db.util import check_media_slug_used, check_collection_slug_used
- from mediagoblin.db.models import User, Client, AccessToken
 -from mediagoblin.db.models import User, Location
++from mediagoblin.db.models import User, Client, AccessToken, Location
  
  import mimetypes
  
@@@ -202,14 -200,29 +202,29 @@@ def edit_profile(request, url_user=None
  
      user = url_user
  
+     # Get the location name
+     if user.location is None:
+         location = ""
+     else:
+         location = user.get_location.name
      form = forms.EditProfileForm(request.form,
          url=user.url,
-         bio=user.bio)
+         bio=user.bio,
+         location=location)
  
      if request.method == 'POST' and form.validate():
 -        user.url = unicode(form.url.data)
 -        user.bio = unicode(form.bio.data)
 +        user.url = six.text_type(form.url.data)
 +        user.bio = six.text_type(form.bio.data)
  
+         # Save location
+         if form.location.data and user.location is None:
+             user.get_location = Location(name=unicode(form.location.data))
+         elif form.location.data:
+             location = user.get_location.name
+             location.name = unicode(form.location.data)
+             location.save()
          user.save()
  
          messages.add_message(request,
@@@ -480,10 -465,10 +495,10 @@@ def edit_metadata(request, media)
          json_ld_metadata = compact_and_validate(metadata_dict)
          media.media_metadata = json_ld_metadata
          media.save()
-         return redirect_obj(request, media)      
+         return redirect_obj(request, media)
  
      if len(form.media_metadata) == 0:
 -        for identifier, value in media.media_metadata.iteritems():
 +        for identifier, value in six.iteritems(media.media_metadata):
              if identifier == "@context": continue
              form.media_metadata.append_entry({
                  'identifier':identifier,
Simple merge
index a941e5603c980f0cf778728f5ddb9851b7e9de88,6dd540e5200b9f56d872f11c8296c1e9019b8a68..e0ddfe87705437a940f14545cfc5a6c13c83bebe
@@@ -24,9 -22,8 +24,10 @@@ import o
  import logging
  import argparse
  
 +import six
 +
  from mediagoblin import mg_globals as mgg
+ from mediagoblin.db.models import Location
  from mediagoblin.processing import (
      BadMediaFail, FilenameBuilder,
      MediaProcessor, ProcessingManager,
Simple merge