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