diff options
author | Jessica Tallon <jessica@megworld.co.uk> | 2014-10-09 19:20:13 +0100 |
---|---|---|
committer | Jessica Tallon <jessica@megworld.co.uk> | 2014-10-09 19:20:13 +0100 |
commit | ed48454558a91961b6e03fc51b8a4bf785d48d1e (patch) | |
tree | 792294c842fe5643a2d8f66be366805f621d4ece | |
parent | 9a1fc423ac298c2ddf078d91ea1302c135285781 (diff) | |
parent | c0434db46910e891313495b5ae94cbbe1dd08058 (diff) | |
download | mediagoblin-ed48454558a91961b6e03fc51b8a4bf785d48d1e.tar.lz mediagoblin-ed48454558a91961b6e03fc51b8a4bf785d48d1e.tar.xz mediagoblin-ed48454558a91961b6e03fc51b8a4bf785d48d1e.zip |
Merge branch 'location'
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.
-rw-r--r-- | mediagoblin/db/migrations.py | 37 | ||||
-rw-r--r-- | mediagoblin/db/models.py | 116 | ||||
-rw-r--r-- | mediagoblin/edit/forms.py | 1 | ||||
-rw-r--r-- | mediagoblin/edit/views.py | 21 | ||||
-rw-r--r-- | mediagoblin/federation/views.py | 14 | ||||
-rw-r--r-- | mediagoblin/media_types/image/migrations.py | 56 | ||||
-rw-r--r-- | mediagoblin/media_types/image/models.py | 4 | ||||
-rw-r--r-- | mediagoblin/media_types/image/processing.py | 4 | ||||
-rw-r--r-- | mediagoblin/plugins/geolocation/__init__.py | 6 | ||||
-rw-r--r-- | mediagoblin/plugins/geolocation/templates/mediagoblin/plugins/geolocation/map.html | 13 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/user_pages/media.html | 3 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/user_pages/user.html | 2 | ||||
-rw-r--r-- | mediagoblin/templates/mediagoblin/utils/profile.html | 12 | ||||
-rw-r--r-- | mediagoblin/tests/test_submission.py | 2 |
14 files changed, 266 insertions, 25 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py index 31b8333e..0e0ee6be 100644 --- a/mediagoblin/db/migrations.py +++ b/mediagoblin/db/migrations.py @@ -1086,3 +1086,40 @@ def activity_migration(db): ).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() diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py index 0069c85a..b1bdba88 100644 --- a/mediagoblin/db/models.py +++ b/mediagoblin/db/models.py @@ -45,6 +45,79 @@ import six _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): """ @@ -71,6 +144,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") activity = Column(Integer, ForeignKey("core__activity_intermediators.id")) @@ -175,9 +250,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 @@ -265,6 +349,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) @@ -476,6 +562,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()] @@ -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, @@ -666,6 +760,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): @@ -692,6 +789,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 @@ -710,6 +811,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) @@ -889,9 +993,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): """ @@ -1243,6 +1346,10 @@ class Activity(Base, ActivityMixin): 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, @@ -1250,7 +1357,8 @@ MODELS = [ CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan, Privilege, PrivilegeUserAssociation, RequestToken, AccessToken, NonceTimestamp, - Activity, ActivityIntermediator, Generator] + Activity, ActivityIntermediator, Generator, + 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 2ccf11ae..30a32a7e 100644 --- a/mediagoblin/edit/views.py +++ b/mediagoblin/edit/views.py @@ -47,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, Client, AccessToken, Location import mimetypes @@ -202,14 +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 = 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,7 +495,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 six.iteritems(media.media_metadata): diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py index 370ec8c3..4c0593fc 100644 --- a/mediagoblin/federation/views.py +++ b/mediagoblin/federation/views.py @@ -208,6 +208,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) @@ -303,6 +308,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 <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() 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 a941e560..e0ddfe87 100644 --- a/mediagoblin/media_types/image/processing.py +++ b/mediagoblin/media_types/image/processing.py @@ -27,6 +27,7 @@ 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, @@ -235,8 +236,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 %} - <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" diff --git a/mediagoblin/templates/mediagoblin/user_pages/media.html b/mediagoblin/templates/mediagoblin/user_pages/media.html index 0b2ae898..a4b42211 100644 --- a/mediagoblin/templates/mediagoblin/user_pages/media.html +++ b/mediagoblin/templates/mediagoblin/user_pages/media.html @@ -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 %} @@ -231,6 +232,8 @@ {% block mediagoblin_sidebar %} {% endblock %} + {%- set model = media %} + {% template_hook("location_info") %} {% template_hook("media_sideinfo") %} </div><!--end media_sidebar--> 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 -%} </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> 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 <http://www.gnu.org/licenses/>. #} +{% block mediagoblin_head %} + {% template_hook("location_head") %} +{% endblock mediagoblin_head %} + {% block profile_content -%} {% if user.bio %} {% autoescape False %} @@ -27,4 +31,12 @@ <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 %} diff --git a/mediagoblin/tests/test_submission.py b/mediagoblin/tests/test_submission.py index a671d672..03d255fb 100644 --- a/mediagoblin/tests/test_submission.py +++ b/mediagoblin/tests/test_submission.py @@ -363,7 +363,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[ |