From c0434db46910e891313495b5ae94cbbe1dd08058 Mon Sep 17 00:00:00 2001 From: Jessica Tallon Date: Mon, 15 Sep 2014 19:34:42 +0100 Subject: [PATCH] Add location model and migrations --- mediagoblin/db/migrations.py | 37 ++++++ mediagoblin/db/models.py | 119 +++++++++++++++++- mediagoblin/edit/forms.py | 1 + mediagoblin/edit/views.py | 21 +++- mediagoblin/federation/views.py | 18 ++- mediagoblin/media_types/image/migrations.py | 56 +++++++++ mediagoblin/media_types/image/models.py | 4 - mediagoblin/media_types/image/processing.py | 4 +- mediagoblin/plugins/geolocation/__init__.py | 6 +- .../mediagoblin/plugins/geolocation/map.html | 13 +- .../mediagoblin/user_pages/media.html | 3 + .../mediagoblin/user_pages/user.html | 2 +- .../templates/mediagoblin/utils/profile.html | 12 ++ mediagoblin/tests/test_submission.py | 2 +- 14 files changed, 270 insertions(+), 28 deletions(-) diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 04588ad1..be509a75 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -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() diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index b910e522..20eccdae 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -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 diff --git a/mediagoblin/edit/forms.py b/mediagoblin/edit/forms.py index c0bece8b..f0a03e04 100644 --- a/mediagoblin/edit/forms.py +++ b/mediagoblin/edit/forms.py @@ -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( diff --git a/mediagoblin/edit/views.py b/mediagoblin/edit/views.py index e998d6be..17c83d46 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -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(): diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py index 3d6953a7..5b27144d 100644 --- a/mediagoblin/federation/views.py +++ b/mediagoblin/federation/views.py @@ -70,14 +70,14 @@ def profile_endpoint(request): def user_endpoint(request): """ This is /api/user/ - 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( diff --git a/mediagoblin/media_types/image/migrations.py b/mediagoblin/media_types/image/migrations.py index f54c23ea..4af8f298 100644 --- a/mediagoblin/media_types/image/migrations.py +++ b/mediagoblin/media_types/image/migrations.py @@ -13,5 +13,61 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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() diff --git a/mediagoblin/media_types/image/models.py b/mediagoblin/media_types/image/models.py index b2ea3960..02040ec2 100644 --- a/mediagoblin/media_types/image/models.py +++ b/mediagoblin/media_types/image/models.py @@ -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 diff --git a/mediagoblin/media_types/image/processing.py b/mediagoblin/media_types/image/processing.py index 1db82ee7..6dd540e5 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -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) diff --git a/mediagoblin/plugins/geolocation/__init__.py b/mediagoblin/plugins/geolocation/__init__.py index 5d14590e..06aab68e 100644 --- a/mediagoblin/plugins/geolocation/__init__.py +++ b/mediagoblin/plugins/geolocation/__init__.py @@ -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 = { diff --git a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html index 70f837ff..8da6f0ee 100644 --- a/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html +++ b/mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html @@ -17,14 +17,13 @@ #} {% 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 %} -

{% trans %}Location{% endtrans %}

+ {% if model.location + and model.get_location.position + and model.get_location.position.latitude + and model.get_location.position.longitude %}
- {%- 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) %}
+ {% template_hook("location_head") %} {% template_hook("media_head") %} {% endblock mediagoblin_head %} {% block mediagoblin_content %} @@ -231,6 +232,8 @@ {% block mediagoblin_sidebar %} {% endblock %} + {%- set model = media %} + {% template_hook("location_info") %} {% template_hook("media_sideinfo") %}
diff --git a/mediagoblin/templates/mediagoblin/user_pages/user.html b/mediagoblin/templates/mediagoblin/user_pages/user.html index 51baa9bb..9ac96f80 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/user.html +++ b/mediagoblin/templates/mediagoblin/user_pages/user.html @@ -46,7 +46,7 @@ {%- trans username=user.username %}{{ username }}'s profile{% endtrans -%} - {% 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) %}

diff --git a/mediagoblin/templates/mediagoblin/utils/profile.html b/mediagoblin/templates/mediagoblin/utils/profile.html index 7a3af01c..5cc38487 100644 --- a/mediagoblin/templates/mediagoblin/utils/profile.html +++ b/mediagoblin/templates/mediagoblin/utils/profile.html @@ -16,6 +16,10 @@ # along with this program. If not, see . #} +{% block mediagoblin_head %} + {% template_hook("location_head") %} +{% endblock mediagoblin_head %} + {% block profile_content -%} {% if user.bio %} {% autoescape False %} @@ -27,4 +31,12 @@ {{ user.url }}

{% endif %} + {% if user.location %} + {%- set model = user %} +

{% trans %}Location{% endtrans %}

+ {% if model.get_location.name %} +

{{ model.get_location.name }}

+ {% endif %} + {% template_hook("location_info") %} + {% endif %} {% endblock %} diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index b5b13ed3..36e57b8c 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -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[ -- 2.25.1