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.
This commit is contained in:
commit
ed48454558
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -26,8 +26,8 @@ def setup_plugin():
|
||||
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 = {
|
||||
|
@ -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"
|
||||
|
@ -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-->
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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[
|
||||
|
Loading…
x
Reference in New Issue
Block a user