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]
## 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
- 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 = []
def __init__(self, *args, **kwargs):
self._required = []
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
@ -32,7 +33,7 @@ class BaseEntity(object):
required_fulfilled = set(self._required).issubset(set(attributes))
if not required_fulfilled:
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"]
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")
@ -157,7 +158,7 @@ class Reaction(GUIDMixin, ParticipationMixin, CreatedAtMixin, HandleMixin):
class Relationship(CreatedAtMixin, HandleMixin):
"""Represents a """
"""Represents a relationship between two handles."""
target_handle = ""
relationship = ""
@ -178,3 +179,28 @@ class Relationship(CreatedAtMixin, HandleMixin):
raise ValueError("relationship should be one of: {valid}".format(
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 -*-
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
@ -73,3 +73,26 @@ class DiasporaRequest(DiasporaEntityMixin, Relationship):
{"recipient_handle": self.target_handle},
])
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 federation.entities.base import Image, Relationship, Post, Reaction, Comment
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest
from federation.entities.base import Image, Relationship, Post, Reaction, Comment, Profile
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest, \
DiasporaProfile
MAPPINGS = {
"status_message": DiasporaPost,
@ -12,10 +13,12 @@ MAPPINGS = {
"comment": DiasporaComment,
"like": DiasporaLike,
"request": DiasporaRequest,
"profile": DiasporaProfile,
}
BOOLEAN_KEYS = [
"public",
"nsfw",
]
DATETIME_KEYS = [
@ -62,12 +65,32 @@ def transform_attributes(attrs):
transformed["target_handle"] = value
elif key == "parent_guid":
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:
transformed[key] = True if value == "true" else False
elif key in DATETIME_KEYS:
transformed[key] = datetime.strptime(value, "%Y-%m-%d %H:%M:%S %Z")
else:
transformed[key] = value
transformed[key] = value or ""
return transformed
@ -84,7 +107,7 @@ def get_outbound_entity(entity):
An instance of the correct protocol specific entity.
"""
cls = entity.__class__
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike]:
if cls in [DiasporaPost, DiasporaRequest, DiasporaComment, DiasporaLike, DiasporaProfile]:
# Already fine
return entity
elif cls == Post:
@ -98,4 +121,6 @@ def get_outbound_entity(entity):
if entity.relationship in ["sharing", "following"]:
# Unfortunately we must send out in both cases since in Diaspora they are the same thing
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.")

Wyświetl plik

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
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):
@ -41,3 +42,19 @@ class TestEntitiesConvertToXML(object):
converted = b"<request><sender_handle>bob@example.com</sender_handle>" \
b"<recipient_handle>alice@example.com</recipient_handle></request>"
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
from federation.entities.base import Comment, Post, Reaction, Relationship
from federation.entities.diaspora.entities import DiasporaPost, DiasporaComment, DiasporaLike, DiasporaRequest
from federation.entities.base import Comment, Post, Reaction, Relationship, Profile
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.tests.fixtures.payloads import DIASPORA_POST_SIMPLE, DIASPORA_POST_COMMENT, DIASPORA_POST_LIKE, \
DIASPORA_REQUEST
DIASPORA_REQUEST, DIASPORA_PROFILE
class TestDiasporaEntityMappersReceive(object):
@ -63,6 +64,24 @@ class TestDiasporaEntityMappersReceive(object):
assert sharing.relationship == "sharing"
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):
def test_already_fine_entities_are_returned_as_is(self):
@ -74,6 +93,8 @@ class TestGetOutboundEntity(object):
assert get_outbound_entity(entity) == entity
entity = DiasporaRequest()
assert get_outbound_entity(entity) == entity
entity = DiasporaProfile()
assert get_outbound_entity(entity) == entity
def test_post_is_converted_to_diasporapost(self):
entity = Post()
@ -93,6 +114,10 @@ class TestGetOutboundEntity(object):
entity = Relationship(relationship="following")
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):
entity = Reaction(reaction="foo")
with pytest.raises(ValueError):

Wyświetl plik

@ -3,7 +3,7 @@ from unittest.mock import Mock
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
@ -47,3 +47,18 @@ class TestRelationshipEntity(object):
with pytest.raises(ValueError):
entity = Relationship(handle="bob@example.com", target_handle="fefle.com", relationship="following")
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>
</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>
"""