Add location model and migrations
authorJessica Tallon <jessica@megworld.co.uk>
Mon, 15 Sep 2014 18:34:42 +0000 (19:34 +0100)
committerJessica Tallon <jessica@megworld.co.uk>
Thu, 9 Oct 2014 18:16:54 +0000 (19:16 +0100)
14 files changed:
mediagoblin/db/migrations.py
mediagoblin/db/models.py
mediagoblin/edit/forms.py
mediagoblin/edit/views.py
mediagoblin/federation/views.py
mediagoblin/media_types/image/migrations.py
mediagoblin/media_types/image/models.py
mediagoblin/media_types/image/processing.py
mediagoblin/plugins/geolocation/__init__.py
mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html
mediagoblin/templates/mediagoblin/user_pages/media.html
mediagoblin/templates/mediagoblin/user_pages/user.html
mediagoblin/templates/mediagoblin/utils/profile.html
mediagoblin/tests/test_submission.py

index 04588ad1462ebd136a6b73e332b81fa7d42124b9..be509a75857fb18784b8889c4e1a328ca01c639e 100644 (file)
@@ -890,3 +890,40 @@ def revert_username_index(db):
             db.rollback()
 
     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(24, 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 b910e522658dcf8b0c621176cc9cf68425fe9264..20eccdaed4b48ac10eba26828cd7600d19ebed23 100644 (file)
@@ -48,6 +48,79 @@ from migrate import changeset
 
 _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):
@@ -75,6 +148,8 @@ class User(Base, UserMixin):
     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
@@ -177,9 +252,18 @@ class User(Base, UserMixin):
             user.update({"summary": self.bio})
         if self.url:
             user.update({"url": self.url})
+        if self.location:
+            user.update({"location": self.get_location.seralize(request)})
 
         return user
 
+    def unserialize(self, data):
+        if "summary" in data:
+            self.bio = data["summary"]
+
+        if "location" in data:
+            Location.create(data, self)
+
 class Client(Base):
     """
         Model representing a client - Used for API Auth
@@ -263,6 +347,8 @@ class MediaEntry(Base, MediaEntryMixin):
         # or use sqlalchemy.types.Enum?
     license = Column(Unicode)
     file_size = Column(Integer, default=0)
+    location = Column(Integer, ForeignKey("core__locations.id"))
+    get_location = relationship("Location", lazy="joined")
 
     fail_error = Column(Unicode)
     fail_metadata = Column(JSONEncoded)
@@ -477,6 +563,9 @@ 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()]
             total = len(comments)
@@ -504,6 +593,9 @@ class MediaEntry(Base, MediaEntryMixin):
         if "license" in data:
             self.license = data["license"]
 
+        if "location" in data:
+            Licence.create(data["location"], self)
+
         return True
 
 class FileKeynames(Base):
@@ -629,6 +721,8 @@ class MediaComment(Base, MediaCommentMixin):
     author = Column(Integer, ForeignKey(User.id), nullable=False)
     created = Column(DateTime, nullable=False, default=datetime.datetime.now)
     content = Column(UnicodeText, nullable=False)
+    location = Column(Integer, ForeignKey("core__locations.id"))
+    get_location = relationship("Location", lazy="joined")
 
     # Cascade: Comments are owned by their creator. So do the full thing.
     # lazy=dynamic: People might post a *lot* of comments,
@@ -663,6 +757,9 @@ class MediaComment(Base, MediaCommentMixin):
             "author": author.serialize(request)
         }
 
+        if self.location:
+            context["location"] = self.get_location.seralize(request)
+
         return context
 
     def unserialize(self, data):
@@ -689,6 +786,10 @@ class MediaComment(Base, MediaCommentMixin):
 
         self.media_entry = media.id
         self.content = data["content"]
+
+        if "location" in data:
+            Location.create(data["location"], self)
+
         return True
 
 
@@ -707,6 +808,9 @@ class Collection(Base, CollectionMixin):
                      index=True)
     description = Column(UnicodeText)
     creator = Column(Integer, ForeignKey(User.id), nullable=False)
+    location = Column(Integer, ForeignKey("core__locations.id"))
+    get_location = relationship("Location", lazy="joined")
+
     # TODO: No of items in Collection. Badly named, can we migrate to num_items?
     items = Column(Integer, default=0)
 
@@ -869,9 +973,8 @@ class ProcessingNotification(Notification):
         'polymorphic_identity': 'processing_notification'
     }
 
-with_polymorphic(
-    Notification,
-    [ProcessingNotification, CommentNotification])
+# the with_polymorphic call has been moved to the bottom above MODELS
+# this is because it causes conflicts with relationship calls.
 
 class ReportBase(Base):
     """
@@ -1054,13 +1157,19 @@ class PrivilegeUserAssociation(Base):
         ForeignKey(Privilege.id),
         primary_key=True)
 
+
+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,
-    RequestToken, AccessToken, NonceTimestamp]
+    Privilege, PrivilegeUserAssociation,
+    RequestToken, AccessToken, NonceTimestamp,
+    Location]
 
 """
  Foundations are the default rows that are created immediately after the tables
index c0bece8b7e972bb2231d230bac47240c70f99d7f..f0a03e042ee43d7111f6a4d5d33a47db8ddafc55 100644 (file)
@@ -61,6 +61,7 @@ class EditProfileForm(wtforms.Form):
         [wtforms.validators.Optional(),
          wtforms.validators.URL(message=_("This address contains errors"))])
 
+    location = wtforms.TextField(_('Hometown'))
 
 class EditAccountForm(wtforms.Form):
     wants_comment_notification = wtforms.BooleanField(
index e998d6be4fa6e721db7fdb1fcba748c460b14457..17c83d465574b24b7fc252c2b1668b347ddc0f3b 100644 (file)
@@ -45,7 +45,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
+from mediagoblin.db.models import User, Location
 
 import mimetypes
 
@@ -200,14 +200,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)
 
+        # 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,
@@ -450,7 +465,7 @@ 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():
index 3d6953a7c3642f124c2cd0c4013923b40e6a5a5b..5b27144d19d76a47baadb22de13f45cf9bf87f53 100644 (file)
@@ -70,14 +70,14 @@ def profile_endpoint(request):
 def user_endpoint(request):
     """ This is /api/user/<username> - This will get the user """
     user, user_profile = get_profile(request)
-    
+
     if user is None:
         username = request.matchdict["username"]
         return json_error(
             "No such 'user' with username '{0}'".format(username),
             status=404
         )
-    
+
     return json_response({
         "nickname": user.username,
         "updated": user.created.isoformat(),
@@ -207,6 +207,11 @@ def feed_endpoint(request):
                         "Invalid 'image' with id '{0}'".format(media_id)
                     )
 
+
+                # Add location if one exists
+                if "location" in data:
+                    Location.create(data["location"], self)
+
                 media.save()
                 api_add_to_feed(request, media)
 
@@ -302,6 +307,15 @@ def feed_endpoint(request):
                     "object": image.serialize(request),
                 }
                 return json_response(activity)
+            elif obj["objectType"] == "person":
+                # check this is the same user
+                if "id" not in obj or obj["id"] != requested_user.id:
+                    return json_error(
+                        "Incorrect user id, unable to update"
+                    )
+
+                requested_user.unserialize(obj)
+                requested_user.save()
 
     elif request.method != "GET":
         return json_error(
index f54c23ea182561ff4a961e934f804388eeb6b240..4af8f298ad1b30f482b2fe9b3c208916a8134014 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 json
+
+from sqlalchemy import MetaData, Column, ForeignKey
+
+from mediagoblin.db.migration_tools import RegisterMigration, inspect_table
+
 
 MIGRATIONS = {}
+
+@RegisterMigration(1, MIGRATIONS)
+def remove_gps_from_image(db):
+    """
+    This will remove GPS coordinates from the image model to put them
+    on the new Location model.
+    """
+    metadata = MetaData(bind=db.bind)
+    image_table = inspect_table(metadata, "image__mediadata")
+    location_table = inspect_table(metadata, "core__locations")
+    media_entires_table = inspect_table(metadata, "core__media_entries")
+
+    # First do the data migration
+    for row in db.execute(image_table.select()):
+        fields = {
+            "longitude": row.gps_longitude,
+            "latitude": row.gps_latitude,
+            "altitude": row.gps_altitude,
+            "direction": row.gps_direction,
+        }
+
+        # Remove empty values
+        for k, v in fields.items():
+            if v is None:
+                del fields[k]
+
+        # No point in adding empty locations
+        if not fields:
+            continue
+
+        # JSONEncoded is actually a string field just json.dumped
+        # without the ORM we're responsible for that.
+        fields = json.dumps(fields)
+
+        location = db.execute(location_table.insert().values(position=fields))
+
+        # now store the new location model on Image
+        db.execute(media_entires_table.update().values(
+            location=location.inserted_primary_key[0]
+        ).where(media_entires_table.c.id==row.media_entry))
+
+    db.commit()
+
+    # All that data has been migrated across lets remove the fields
+    image_table.columns["gps_longitude"].drop()
+    image_table.columns["gps_latitude"].drop()
+    image_table.columns["gps_altitude"].drop()
+    image_table.columns["gps_direction"].drop()
+
+    db.commit()
index b2ea39609bb42a036d354300647a758909081af0..02040ec27df6cbaf2a84c294a229e4a8df6a88aa 100644 (file)
@@ -39,10 +39,6 @@ class ImageData(Base):
     width = Column(Integer)
     height = Column(Integer)
     exif_all = Column(JSONEncoded)
-    gps_longitude = Column(Float)
-    gps_latitude = Column(Float)
-    gps_altitude = Column(Float)
-    gps_direction = Column(Float)
 
 
 DATA_MODEL = ImageData
index 1db82ee79667024a88c8c09d88a26aa86efe99f0..6dd540e5200b9f56d872f11c8296c1e9019b8a68 100644 (file)
@@ -23,6 +23,7 @@ import logging
 import argparse
 
 from mediagoblin import mg_globals as mgg
+from mediagoblin.db.models import Location
 from mediagoblin.processing import (
     BadMediaFail, FilenameBuilder,
     MediaProcessor, ProcessingManager,
@@ -231,8 +232,7 @@ class CommonImageProcessor(MediaProcessor):
             self.entry.media_data_init(exif_all=exif_all)
 
         if len(gps_data):
-            for key in list(gps_data.keys()):
-                gps_data['gps_' + key] = gps_data.pop(key)
+            Location.create({"position": gps_data}, self.entry)
             self.entry.media_data_init(**gps_data)
 
 
index 5d14590efd95e59c79828cc1f200ba4f9cbf654d..06aab68e035163f21eaa0b36e1cfff016b7f9d75 100644 (file)
@@ -21,13 +21,13 @@ PLUGIN_DIR = os.path.dirname(__file__)
 
 def setup_plugin():
     config = pluginapi.get_config('mediagoblin.plugins.geolocation')
-    
+
     # Register the template path.
     pluginapi.register_template_path(os.path.join(PLUGIN_DIR, 'templates'))
 
     pluginapi.register_template_hooks(
-        {"image_sideinfo": "mediagoblin/plugins/geolocation/map.html",
-         "image_head": "mediagoblin/plugins/geolocation/map_js_head.html"})
+        {"location_info": "mediagoblin/plugins/geolocation/map.html",
+         "location_head": "mediagoblin/plugins/geolocation/map_js_head.html"})
 
 
 hooks = {
index 70f837ff2db02412866961c5756234886e35f9ab..8da6f0ee4a0099a4565ba203c6e6759f90620d9a 100644 (file)
 #}
 
 {% block geolocation_map %}
-  {% if     media.media_data.gps_latitude is defined
-        and media.media_data.gps_latitude
-        and media.media_data.gps_longitude is defined
-        and media.media_data.gps_longitude %}
-    <h3>{% trans %}Location{% endtrans %}</h3>
+  {% if model.location
+        and model.get_location.position
+        and model.get_location.position.latitude
+        and model.get_location.position.longitude %}
     <div>
-      {%- set lon = media.media_data.gps_longitude %}
-      {%- set lat = media.media_data.gps_latitude %}
+      {%- set lon = model.get_location.position.longitude %}
+      {%- set lat = model.get_location.position.latitude %}
       {%- set osm_url = "http://openstreetmap.org/?mlat={lat}&mlon={lon}".format(lat=lat, lon=lon) %}
         <div id="tile-map" style="width: 100%; height: 196px;">
          <input type="hidden" id="gps-longitude"
index 0b2ae89843b3472c6f87210770e561ea6fd8528b..a4b42211c6c2b87195e08dcb8d3dad94cf58e5ab 100644 (file)
@@ -30,6 +30,7 @@
   <script type="text/javascript"
           src="{{ request.staticdirect('/js/keyboard_navigation.js') }}"></script>
 
+  {% template_hook("location_head") %}
   {% template_hook("media_head") %}
 {% endblock mediagoblin_head %}
 {% block mediagoblin_content %}
     {% block mediagoblin_sidebar %}
     {% endblock %}
 
+    {%- set model = media %}
+    {% template_hook("location_info") %}
     {% template_hook("media_sideinfo") %}
 
   </div><!--end media_sidebar-->
index 51baa9bb5b1cae253d0e950c228add94b0d62c6b..9ac96f808403767306f7c0b5fdb334801c79ef1c 100644 (file)
@@ -46,7 +46,7 @@
     {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%}
   </h1>
 
-  {% if not user.url and not user.bio %}
+  {% if not user.url and not user.bio and not user.location %}
     {% if request.user and (request.user.id == user.id) %}
       <div class="profile_sidebar empty_space">
         <p>
index 7a3af01ce2caaf0a2519b139128bf40d829747ea..5cc38487479a93983688233a0bd2d3e200c1edf5 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #}
 
+{% block mediagoblin_head %}
+  {% template_hook("location_head") %}
+{% endblock mediagoblin_head %}
+
 {% block profile_content -%}
   {% if user.bio %}
     {% autoescape False %}
       <a href="{{ user.url }}">{{ user.url }}</a>
     </p>
   {% endif %}
+  {% if user.location %}
+      {%- set model = user %}
+      <h3>{% trans %}Location{% endtrans %}</h3>
+      {% if model.get_location.name %}
+        <p>{{ model.get_location.name }}</p>
+      {% endif %}
+      {% template_hook("location_info") %}
+  {% endif %}
 {% endblock %}
index b5b13ed3c6c1529387b11e53aa0ffdd9cee9b270..36e57b8c287d8fafffd4ad2449433b99ccf774bf 100644 (file)
@@ -359,7 +359,7 @@ class TestSubmission:
     def test_media_data(self):
         self.check_normal_upload(u"With GPS data", GPS_JPG)
         media = self.check_media(None, {"title": u"With GPS data"}, 1)
-        assert media.media_data.gps_latitude == 59.336666666666666
+        assert media.get_location.position["latitude"] == 59.336666666666666
 
     def test_processing(self):
         public_store_dir = mg_globals.global_config[