Add Profile entity

Closes #24
merge-requests/130/head
Jason Robinson 2016-07-19 22:43:56 +03:00
rodzic 08e0abf180
commit 3c27abf0a9
8 zmienionych plików z 166 dodań i 14 usunięć

Wyświetl plik

@ -1,7 +1,8 @@
## [unreleased] ## [unreleased]
## Added ## Added
- Relationship base entity which represents relationships between two handles. Types can be following, sharing, ignoring and blocking. The Diaspora counterpart, DiasporaRequest, which represents a sharing/following request is outwards a single entity, but incoming a double entity, handled by creating both a sharing and following version of the relationship. - `Relationship` base entity which represents relationships between two handles. Types can be following, sharing, ignoring and blocking. The Diaspora counterpart, `DiasporaRequest`, which represents a sharing/following request is outwards a single entity, but incoming a double entity, handled by creating both a sharing and following version of the relationship.
- `Profile` base entity and Diaspora counterpart `DiasporaProfile`. Represents a user profile.
## Changed ## Changed
- Unlock most of the direct dependencies to a certain version range. Unlock all of test requirements to any version. - Unlock most of the direct dependencies to a certain version range. Unlock all of test requirements to any version.

Wyświetl plik

@ -11,6 +11,7 @@ class BaseEntity(object):
_required = [] _required = []
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._required = []
for key, value in kwargs.items(): for key, value in kwargs.items():
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
@ -32,7 +33,7 @@ class BaseEntity(object):
required_fulfilled = set(self._required).issubset(set(attributes)) required_fulfilled = set(self._required).issubset(set(attributes))
if not required_fulfilled: if not required_fulfilled:
raise ValueError( raise ValueError(
"Not all required attributes fulfilled. Required: {required}".format(required=self._required) "Not all required attributes fulfilled. Required: {required}".format(required=set(self._required))
) )
@ -44,7 +45,7 @@ class GUIDMixin(BaseEntity):
self._required += ["guid"] self._required += ["guid"]
def validate_guid(self): def validate_guid(self):
if len(self.guid) < 16: if self.guid and len(self.guid) < 16:
raise ValueError("GUID must be at least 16 characters") raise ValueError("GUID must be at least 16 characters")
@ -157,7 +158,7 @@ class Reaction(GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin):
class Relationship(CreatedAtMixin, HandleMixin): class Relationship(CreatedAtMixin, HandleMixin):
"""Represents a """ """Represents a relationship between two handles."""
target_handle = "" target_handle = ""
relationship = "" relationship = ""
@ -178,3 +179,28 @@ class Relationship(CreatedAtMixin, HandleMixin):
raise ValueError("relationship should be one of: {valid}".format( raise ValueError("relationship should be one of: {valid}".format(
valid=", ".join(self._relationship_valid_values) valid=", ".join(self._relationship_valid_values)
)) ))
class Profile(CreatedAtMixin, HandleMixin, RawContentMixin, PublicMixin, GUIDMixin):
"""Represents a profile for a user."""
name = ""
email = ""
image_urls = {
"small": "", "medium": "", "large": ""
}
gender = ""
location = ""
nsfw = False
tag_list = []
public_key = ""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Don't require a guid for Profile
self._required.remove("guid")
def validate_email(self):
if self.email:
validator = Email()
if not validator.is_valid(self.email):
raise ValueError("Email is not valid")

Wyświetl plik

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from lxml import etree from lxml import etree
from federation.entities.base import Comment, Post, Reaction, Relationship from federation.entities.base import Comment, Post, Reaction, Relationship, Profile
from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes from federation.entities.diaspora.utils import format_dt, struct_to_xml, get_base_attributes
@ -73,3 +73,26 @@ class DiasporaRequest(DiasporaEntityMixin, Relationship):
{"recipient_handle": self.target_handle}, {"recipient_handle": self.target_handle},
]) ])
return element return element
class DiasporaProfile(DiasporaEntityMixin, Profile):
"""Diaspora profile."""
def to_xml(self):
"""Convert to XML message."""
element = etree.Element("profile")
struct_to_xml(element, [
{"diaspora_handle": self.handle},
{"first_name": self.name},
{"last_name": ""}, # Not used in Diaspora modern profiles
{"image_url": self.image_urls["large"]},
{"image_url_small": self.image_urls["small"]},
{"image_url_medium": self.image_urls["medium"]},
{"gender": self.gender},
{"bio": self.raw_content},
{"location": self.location},
{"searchable": "true" if self.public else "false"},
{"nsfw": "true" if self.nsfw else "false"},
{"tag_string": " ".join(["#%s" % tag for tag in self.tag_list])},
])
return element

Wyświetl plik

@ -3,8 +3,9 @@ from datetime import datetime
from lxml import etree from lxml import etree
from federation.entities.base import Image, Relationship, Post, Reaction, Comment from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, \
DiasporaProfile
MAPPINGS = { MAPPINGS = {
"status_message": DiasporaPost, "status_message": DiasporaPost,
@ -12,10 +13,12 @@ MAPPINGS = {
"comment": DiasporaComment, "comment": DiasporaComment,
"like": DiasporaLike, "like": DiasporaLike,
"request": DiasporaRequest, "request": DiasporaRequest,
"profile": DiasporaProfile,
} }
BOOLEAN_KEYS = [ BOOLEAN_KEYS = [
"public", "public",
"nsfw",
] ]
DATETIME_KEYS = [ DATETIME_KEYS = [
@ -62,12 +65,32 @@ def transform_attributes(attrs):
transformed["target_handle"] = value transformed["target_handle"] = value
elif key == "parent_guid": elif key == "parent_guid":
transformed["target_guid"] = value transformed["target_guid"] = value
elif key == "first_name":
transformed["name"] = value
elif key == "image_url":
if "image_urls" not in transformed:
transformed["image_urls"] = {}
transformed["image_urls"]["large"] = value
elif key == "image_url_small":
if "image_urls" not in transformed:
transformed["image_urls"] = {}
transformed["image_urls"]["small"] = value
elif key == "image_url_medium":
if "image_urls" not in transformed:
transformed["image_urls"] = {}
transformed["image_urls"]["medium"] = value
elif key == "tag_string":
transformed["tag_list"] = value.replace("#", "").split(" ")
elif key == "bio":
transformed["raw_content"] = value
elif key == "searchable":
transformed["public"] = True if value == "true" else False
elif key in BOOLEAN_KEYS: elif key in BOOLEAN_KEYS:
transformed[key] = True if value == "true" else False transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS: elif key in DATETIME_KEYS:
transformed[key] = datetime.strptime(value, "%Y-%m-%d %H:%M:%S %Z") transformed[key] = datetime.strptime(value, "%Y-%m-%d %H:%M:%S %Z")
else: else:
transformed[key] = value transformed[key] = value or ""
return transformed return transformed
@ -84,7 +107,7 @@ def get_outbound_entity(entity):
An instance of the correct protocol specific entity. An instance of the correct protocol specific entity.
""" """
cls = entity.__class__ cls = entity.__class__
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike]: if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile]:
# Already fine # Already fine
return entity return entity
elif cls == Post: elif cls == Post:
@ -98,4 +121,6 @@ def get_outbound_entity(entity):
if entity.relationship in ["sharing", "following"]: if entity.relationship in ["sharing", "following"]:
# Unfortunately we must send out in both cases since in Diaspora they are the same thing # Unfortunately we must send out in both cases since in Diaspora they are the same thing
return DiasporaRequest.from_base(entity) return DiasporaRequest.from_base(entity)
elif cls == Profile:
return DiasporaProfile.from_base(entity)
raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.") raise ValueError("Don't know how to convert this base entity to Diaspora protocol entities.")

Wyświetl plik

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from lxml import etree from lxml import etree
from federation.entities.diaspora.entities import DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest from federation.entities.diaspora.entities import DiasporaComment, DiasporaPost, DiasporaLike, DiasporaRequest, \
DiasporaProfile
class TestEntitiesConvertToXML(object): class TestEntitiesConvertToXML(object):
@ -41,3 +42,19 @@ class TestEntitiesConvertToXML(object):
converted = b"<request><sender_handle>bob@example.com</sender_handle>" \ converted = b"<request><sender_handle>bob@example.com</sender_handle>" \
b"<recipient_handle>alice@example.com</recipient_handle></request>" b"<recipient_handle>alice@example.com</recipient_handle></request>"
assert etree.tostring(result) == converted assert etree.tostring(result) == converted
def test_profile_to_xml(self):
entity = DiasporaProfile(
handle="bob@example.com", raw_content="foobar", name="Bob Bobertson", public=True,
tag_list=["socialfederation", "federation"], image_urls={
"large": "urllarge", "medium": "urlmedium", "small": "urlsmall"
}
)
result = entity.to_xml()
assert result.tag == "profile"
converted = b"<profile><diaspora_handle>bob@example.com</diaspora_handle>" \
b"<first_name>Bob Bobertson</first_name><last_name></last_name><image_url>urllarge</image_url>" \
b"<image_url_small>urlsmall</image_url_small><image_url_medium>urlmedium</image_url_medium>" \
b"<gender></gender><bio>foobar</bio><location></location><searchable>true</searchable>" \
b"<nsfw>false</nsfw><tag_string>#socialfederation #federation</tag_string></profile>"
assert etree.tostring(result) == converted

Wyświetl plik

@ -3,11 +3,12 @@ from datetime import datetime
import pytest import pytest
from federation.entities.base import Comment, Post, Reaction, Relationship from federation.entities.base import Comment, Post, Reaction, Relationship, Profile
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, \
DiasporaProfile
from federation.entities.diaspora.mappers import message_to_objects, get_outbound_entity from federation.entities.diaspora.mappers import message_to_objects, get_outbound_entity
from federation.tests.fixtures.payloads import DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, \ from federation.tests.fixtures.payloads import DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, \
DIASPORA_REQUEST DIASPORA_REQUEST, DIASPORA_PROFILE
class TestDiasporaEntityMappersReceive(object): class TestDiasporaEntityMappersReceive(object):
@ -63,6 +64,24 @@ class TestDiasporaEntityMappersReceive(object):
assert sharing.relationship == "sharing" assert sharing.relationship == "sharing"
assert following.relationship == "following" assert following.relationship == "following"
def test_message_to_objects_profile(self):
entities = message_to_objects(DIASPORA_PROFILE)
assert len(entities) == 1
profile = entities[0]
assert profile.handle == "bob@example.com"
assert profile.name == "Bob Bobertson"
assert profile.image_urls == {
"large": "https://example.com/uploads/images/thumb_large_c833747578b5.jpg",
"medium": "https://example.com/uploads/images/thumb_medium_c8b1aab04f3.jpg",
"small": "https://example.com/uploads/images/thumb_small_c8b147578b5.jpg",
}
assert profile.gender == ""
assert profile.raw_content == "A cool bio"
assert profile.location == "Helsinki"
assert profile.public == True
assert profile.nsfw == False
assert profile.tag_list == ["socialfederation", "federation"]
class TestGetOutboundEntity(object): class TestGetOutboundEntity(object):
def test_already_fine_entities_are_returned_as_is(self): def test_already_fine_entities_are_returned_as_is(self):
@ -74,6 +93,8 @@ class TestGetOutboundEntity(object):
assert get_outbound_entity(entity) == entity assert get_outbound_entity(entity) == entity
entity = DiasporaRequest() entity = DiasporaRequest()
assert get_outbound_entity(entity) == entity assert get_outbound_entity(entity) == entity
entity = DiasporaProfile()
assert get_outbound_entity(entity) == entity
def test_post_is_converted_to_diasporapost(self): def test_post_is_converted_to_diasporapost(self):
entity = Post() entity = Post()
@ -93,6 +114,10 @@ class TestGetOutboundEntity(object):
entity = Relationship(relationship="following") entity = Relationship(relationship="following")
assert isinstance(get_outbound_entity(entity), DiasporaRequest) assert isinstance(get_outbound_entity(entity), DiasporaRequest)
def test_profile_is_converted_to_diasporaprofile(self):
entity = Profile()
assert isinstance(get_outbound_entity(entity), DiasporaProfile)
def test_other_reaction_raises(self): def test_other_reaction_raises(self):
entity = Reaction(reaction="foo") entity = Reaction(reaction="foo")
with pytest.raises(ValueError): with pytest.raises(ValueError):

Wyświetl plik

@ -3,7 +3,7 @@ from unittest.mock import Mock
import pytest import pytest
from federation.entities.base import BaseEntity, Relationship from federation.entities.base import BaseEntity, Relationship, Profile
from federation.tests.factories.entities import TaggedPostFactory, PostFactory from federation.tests.factories.entities import TaggedPostFactory, PostFactory
@ -47,3 +47,18 @@ class TestRelationshipEntity(object):
with pytest.raises(ValueError): with pytest.raises(ValueError):
entity = Relationship(handle="bob@example.com", target_handle="fefle.com", relationship="following") entity = Relationship(handle="bob@example.com", target_handle="fefle.com", relationship="following")
entity.validate() entity.validate()
class TestProfileEntity(object):
def test_instance_creation(self):
entity = Profile(handle="bob@example.com", raw_content="foobar")
assert entity
def test_instance_creation_validates_email_value(self):
with pytest.raises(ValueError):
entity = Profile(handle="bob@example.com", raw_content="foobar", email="foobar")
entity.validate()
def test_guid_is_not_mandatory(self):
entity = Profile(handle="bob@example.com", raw_content="foobar")
entity.validate()

Wyświetl plik

@ -77,3 +77,23 @@ DIASPORA_REQUEST = """<XML>
</post> </post>
</XML> </XML>
""" """
DIASPORA_PROFILE = """<XML>
<post>
<profile>
<diaspora_handle>bob@example.com</diaspora_handle>
<first_name>Bob Bobertson</first_name>
<last_name></last_name>
<image_url>https://example.com/uploads/images/thumb_large_c833747578b5.jpg</image_url>
<image_url_small>https://example.com/uploads/images/thumb_small_c8b147578b5.jpg</image_url_small>
<image_url_medium>https://example.com/uploads/images/thumb_medium_c8b1aab04f3.jpg</image_url_medium>
<gender></gender>
<bio>A cool bio</bio>
<location>Helsinki</location>
<searchable>true</searchable>
<nsfw>false</nsfw>
<tag_string>#socialfederation #federation</tag_string>
</profile>
</post>
</XML>
"""