<reponame>kfarrelly/nucleo
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import os, requests, toml, urlparse

from allauth.utils import build_absolute_uri

from datetime import timedelta

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.db import models
from django.forms.models import model_to_dict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _

from functools import partial

from stellar_base.asset import Asset as StellarAsset
from stellar_base.address import Address
from stellar_base.utils import AccountNotExistError

from timeseries.utils import TimeSeriesModel, TimeSeriesManager

from . import managers, validators


def profile_file_directory_path(instance, filename, field):
    """
    File will be uploaded to MEDIA_ROOT/profile/<user_id>/<field>/<filename>
    """
    file_root, file_ext = os.path.splitext(filename)
    new_file_root = get_random_string().upper()
    new_filename = new_file_root + file_ext
    return 'profile/{0}/{1}/{2}'.format(instance.user_id, field, new_filename)

def model_file_directory_path(instance, filename, field):
    """
    File will be uploaded to MEDIA_ROOT/<model>/<id>/<field>/<filename>
    """
    file_root, file_ext = os.path.splitext(filename)
    new_file_root = get_random_string().upper()
    new_filename = new_file_root + file_ext
    return '{0}/{1}/{2}/{3}'.format(type(instance).__name__.lower(),
        instance.id, field, new_filename)

@python_2_unicode_compatible
class Profile(models.Model):
    """
    Profile associated with a user.
    """
    DEFAULT_PIC_URL = static('nc/images/profile.png')

    user = models.OneToOneField(settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE, related_name='profile', primary_key=True)
    bio = models.CharField(max_length=255, null=True, blank=True, default=None)
    pic = models.ImageField(
        _('Profile picture'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(profile_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )
    cover = models.ImageField(
        _('Cover photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(profile_file_directory_path, field='cover'),
        null=True, blank=True, default=None
    )
    followers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='profiles_following'
    )
    # NOTE: If private profile, must approve follower request for other user to
    # see assets, accounts, etc.
    is_private = models.BooleanField(default=False)
    accounts_created = models.PositiveSmallIntegerField(default=0)

    # Settings
    ## Email
    allow_payment_email = models.BooleanField(default=True)
    allow_token_issuance_email = models.BooleanField(default=True)
    allow_trade_email = models.BooleanField(default=True)
    allow_follower_email = models.BooleanField(default=True)
    allow_trust_email = models.BooleanField(default=True)

    # NOTE: user.get_full_name(), followers.count() are duplicated here
    # so Algolia search index updates work when user, follower updates occur (kept in sync through signals.py)
    full_name = models.CharField(max_length=200, null=True, blank=True, default=None)
    followers_count = models.IntegerField(default=0)

    def username(self):
        """
        Have this method as a proxy for the search index.

        user.username will never change in the db so search index related
        update issues with related fields don't exist here.
        """
        return self.user.username

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return reverse('nc:user-detail', kwargs={'slug': self.user.username})

    def __str__(self):
        bio =  ': ' + self.bio if self.bio else ''
        return self.user.username + bio


@python_2_unicode_compatible
class FollowRequest(models.Model):
    """
    Follow request for users who have a private profile.
    """
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='follower_requests', on_delete=models.CASCADE)
    requester = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='requests_to_follow', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('user', 'requester')

    def __str__(self):
        return 'Follow request: ' + self.requester.username + ' -> ' + self.user.username


@python_2_unicode_compatible
class Account(models.Model):
    """
    Store of Stellar public key addresses associated with user.
    """
    DEFAULT_PIC_URL = static('nc/images/account.png')

    public_key = models.CharField(max_length=56, unique=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='accounts', on_delete=models.CASCADE)
    name = models.CharField(_('Account name'), max_length=150,
        null=True, blank=True, default=None)
    pic = models.ImageField(
        _('Account photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )

    # StellarNotifier subscription id
    notifier_subscription_id = models.CharField(_('StellarNotifier subscription ID'), max_length=150,
        null=True, blank=True, default=None)

    # NOTE: user.get_full_name(), user.pic, profile_is_private duplicated here
    # so Algolia search index updates work when user updates occur (kept in sync through signals.py)
    user_full_name = models.CharField(max_length=200, null=True, blank=True, default=None)
    user_pic_url = models.URLField(null=True, blank=True, default=None)

    def create_notifier_subscription(self, request):
        """
        Create StellarNotifier subscription for this account
        and store the returned ID.

        NOTE: see https://github.com/orbitlens/stellar-notifier#create-subscription
        """
        # POST to the StellarNotifier subscription endpoint
        data = {
            'reaction_url': build_absolute_uri(request, reverse('nc:activity-create')),
            'account': self.public_key
        }
        print data
        r = requests.post(settings.STELLAR_NOTIFIER_SUBSCRIPTION_URL, data=data)

        # Store the returned notifier ID
        ret = {}
        if r.status_code == requests.codes.ok:
            ret = r.json()
            self.notifier_subscription_id = ret.get("id")
            self.save()

        # Return the response from notifier
        return ret

    def remove_notifier_subcription(self):
        """
        Remove StellarNotifier subscription for this account.

        NOTE: see https://github.com/orbitlens/stellar-notifier#remove-subscription
        """
        # DELETE to the StellarNotifier subscription endpoint
        # NOTE: StellarNotifier node js has unhandled promise when fetching subscriptions. WHY? Memory mode only?
        ret = {}
        if self.notifier_subscription_id:
            data = {
                'subscription_id': self.notifier_subscription_id
            }
            url = '{0}{1}'.format(settings.STELLAR_NOTIFIER_SUBSCRIPTION_URL, self.notifier_subscription_id)
            r = requests.delete(url, data=data)
            if r.status_code == requests.codes.ok:
                ret = r.json()

        # Return the response from notifier
        return ret

    def username(self):
        """
        Have this method as a proxy for the search index.

        user.username will never change in the db so search index related
        update issues with related fields don't exist here.
        """
        return self.user.username

    def profile_is_private(self):
        """
        Have this method as a proxy for the search index.

        Returns [0] if profile.is_private and [1] if not private.

        Need this to be a list of ints in order to work with OR bool
        in index.AccountIndex filters alongside viewable_by_if_private.
        """
        is_private_int = 0 if not self.user.profile.is_private else 1
        return [ is_private_int ]

    def viewable_by_if_private(self):
        """
        Have this method as a proxy for the search index to determine who
        can view this account if associated profile is private.

        If private, account can only be seen by those who follow this user's
        profile and the user themselves.

        Will have to call AccountIndex.save_record() if associated profile
        follower status changes. TODO: Use user change signals.
        """
        return [ u.id for u in self.user.profile.followers.all()\
            .union(get_user_model().objects.filter(id=self.user.id)) ]

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return '{}#accounts'.format(reverse('nc:user-detail', kwargs={'slug': self.user.username}))

    def __str__(self):
        name = self.name + ': ' if self.name else ''
        return name + self.public_key


@python_2_unicode_compatible
class AccountFundRequest(models.Model):
    """
    Requests for funding a new Stellar account.
    """
    public_key = models.CharField(max_length=56, unique=True)
    requester = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='requests_to_fund_account', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('public_key', 'requester')

    def __str__(self):
        return 'Funding request: ' + self.requester.username + ' -> ' + self.public_key


@python_2_unicode_compatible
class Asset(models.Model):
    """
    Store of Stellar public asset information so can customize image icon
    plus color scheme.

    If issuer is registered in Nucleo, allow them to change the color, pic
    of asset.
    """
    DEFAULT_PIC_URL = static('nc/images/asset.png')

    # NOTE: If None, then either doesn't have an account with us OR must be Lumens
    issuer = models.ForeignKey(Account, related_name='assets_issued',
        on_delete=models.CASCADE, null=True, blank=True, default=None)

    # NOTE: if None then must be Lumens: https://www.stellar.org/developers/guides/concepts/assets.html
    issuer_address = models.CharField(max_length=56, null=True, blank=True, default=None)
    domain = models.CharField(max_length=255, null=True, blank=True, default=None)
    code = models.CharField(max_length=12)
    asset_id = models.CharField(max_length=70, null=True, blank=True, default=None)

    trusters = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='assets_trusting'
    )

    # NOTE: Have both user and account trusters to differentiate after add/remove trust with button active state.
    account_trusters = models.ManyToManyField(
        Account,
        related_name='assets_trusting'
    )

    # NOTE: Since these meta fields are defined in the TOML (which Nucleo can't change
    # directly for logged in user clientside), should store them here to be safe
    name = models.CharField(max_length=255, null=True, blank=True, default=None)
    description = models.CharField(max_length=255, null=True, blank=True, default=None)
    conditions = models.CharField(max_length=255, null=True, blank=True, default=None)
    display_decimals = models.PositiveSmallIntegerField(default=7)

    pic = models.ImageField(
        _('Asset profile photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )
    cover = models.ImageField(
        _('Asset cover photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='cover'),
        null=True, blank=True, default=None
    )
    whitepaper = models.FileField(
        validators=[validators.validate_pdf_file_extension, validators.validate_file_size,
            validators.validate_pdf_mimetype],
        upload_to=partial(model_file_directory_path, field='whitepaper'),
        null=True, blank=True, default=None
    )

    toml = models.URLField(null=True, blank=True, default=None)
    toml_pic = models.URLField(null=True, blank=True, default=None)
    verified = models.BooleanField(default=False)

    # Asset manager
    objects = managers.AssetManager()

    def issuer_handle(self):
        """
        Have this method as a proxy for the search index.
        """
        if self.issuer:
            issuer_handle = '@' + self.issuer.user.username
        elif self.issuer_address:
            issuer_handle = None
        else:
            issuer_handle = 'Stellar Network'
        return issuer_handle

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        elif self.toml_pic:
            pic_url = self.toml_pic
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return reverse('nc:asset-detail', kwargs={'slug': self.asset_id})

    def type(self):
        """
        Have this method as a proxy for stellar asset type so don't have to
        rewrite guess() from stellar_base.
        """
        stellar_asset = StellarAsset(self.code, self.issuer_address)
        return stellar_asset.type

    def update_from_toml(self, toml_url=None):
        """
        Fetch toml file and update attributes of this instance with its details.
        """
        updated = False

        # Create dict of instance data to check for changes later
        fields_to_update = [ 'verified', 'toml', 'domain', 'toml_pic', 'name',
            'description', 'conditions', 'display_decimals' ]
        old_data = model_to_dict(self, fields=fields_to_update)

        # Set the new toml value
        self.toml = toml_url
        self.domain = urlparse.urlparse(toml_url).netloc if toml_url else None
        if not toml_url:
            self.verified = False

        # Fetch the toml file and check for this asset in [[CURRENCIES]]
        # Use try except in case no toml file exists at given toml_url
        if self.toml:
            try:
                r = requests.get(self.toml)
                parsed_toml = toml.loads(r.text)
                matched_currencies = [ c for c in parsed_toml.get('CURRENCIES', [])
                    if c.get('code', None) == self.code and c.get('issuer') == self.issuer_address ]

                self.verified = (len(matched_currencies) == 1)
                if self.verified:
                    # If matched, then asset has been verified and start updating instance fields
                    currency = matched_currencies[0]
                    if 'image' in currency:
                        self.toml_pic = currency['image']

                    if 'name' in currency:
                        # NOTE: problems here if desc is longer than 255 so concat for db
                        self.name = currency['name'][:255]

                    if 'desc' in currency:
                        # NOTE: problems here if desc is longer than 255 so concat for db
                        self.description = currency['desc'][:255]

                    if 'conditions' in currency:
                        # NOTE: problems here if conditions is longer than 255 so concat for db
                        self.conditions = currency['conditions'][:255]

                    if 'display_decimals' in currency:
                        self.display_decimals = currency['display_decimals']
            except requests.exceptions.ConnectionError:
                self.verified = False

        # Check whether any fields have been updated
        for field in fields_to_update:
            if old_data[field] != getattr(self, field):
                updated = True
                break

        # Save the instance
        if updated:
            self.save()

    class Meta:
        unique_together = ('issuer_address', 'code')

    def __str__(self):
        if self.asset_id:
            asset_id = self.asset_id
        elif self.issuer_address:
            asset_id = '{0}-{1}'.format(self.code, self.issuer_address)
        else:
            asset_id = '{0}-{1}'.format(instance.code, 'native')
        return asset_id


@python_2_unicode_compatible
class Portfolio(models.Model):
    """
    Stores time series data tracking cumulative value (in XLM) user has
    in all accounts associated with their user profile.
    """
    NOT_AVAILABLE = -1.0

    profile = models.OneToOneField(Profile,
        on_delete=models.CASCADE, related_name='portfolio', primary_key=True)

    # Most recent calculated balance values for easy access
    xlm_value = models.FloatField(default=NOT_AVAILABLE)
    usd_value = models.FloatField(default=NOT_AVAILABLE)

    # Performance stats for: 1d, 1w, 1m, 3m, 6m, 1y.
    # NOTE: Fractional values (i.e. need to mult by 100 to get percentages)
    performance_1d = models.FloatField(null=True, blank=True, default=None)
    performance_1w = models.FloatField(null=True, blank=True, default=None)
    performance_1m = models.FloatField(null=True, blank=True, default=None)
    performance_3m = models.FloatField(null=True, blank=True, default=None)
    performance_6m = models.FloatField(null=True, blank=True, default=None)
    performance_1y = models.FloatField(null=True, blank=True, default=None)

    # Rank is ordering of users for 1d performance. Only rank top 100
    rank = models.PositiveIntegerField(null=True, blank=True, default=None)

    # Portfolio manager
    objects = TimeSeriesManager()

    def __str__(self):
        return 'Portfolio: ' + self.profile.user.username


@python_2_unicode_compatible
class RawPortfolioData(TimeSeriesModel):
    # Update every 5 minutes on cron job but put min interval at 2.5 minutes to be safe.
    # NOTE: Cron job taking 1 min at this point with 100 portfolios + 100 assets
    # TODO: Parallelize cron workers for scaling by having separate instances take on smaller subsets of all portfolios?
    TIMESERIES_INTERVAL = timedelta(minutes=2.5)
    NOT_AVAILABLE = -1.0

    portfolio = models.ForeignKey(Portfolio, related_name='rawdata')
    xlm_value = models.FloatField(default=NOT_AVAILABLE)
    usd_value = models.FloatField(default=NOT_AVAILABLE)

    def __str__(self):
        return str(self.portfolio) + ': ' + str(self.usd_value) + ' (' + str(self.created) + ')'


def portfolio_data_collector(queryset, asset_prices):
    """
    Should return an iterable that yields dictionaries of data
    needed to successfully create a RawPortfolioData instance.

    asset_prices is a dict with asset_id: current_market_price, with
    market prices as float.
    If not native, market prices in XLM. Otherwise, in USD.
    """
    # Store the xlm price in USD from asset_prices dict
    usd_xlm_price = asset_prices['XLM-native']

    # Map from model_asset.asset_id to model_asset.id
    # TODO: make model_asset.asset_id unique key
    model_asset_map = {
        asset.asset_id: asset.id
        for asset in Asset.objects.all()
    }

    # Accumulate stellar addresses for each user
    portfolio_addresses = [
        {
            'portfolio': pt,
            'addresses': [
                (a, Address(address=a.public_key, network=settings.STELLAR_NETWORK))
                for a in pt.profile.user.accounts.all()
            ]
        }
        for pt in queryset.prefetch_related('profile__user__accounts')
    ]

    # Retrieve addresses from Horizon then calculate portfolio value
    # given asset balances
    ret = []
    for obj in portfolio_addresses:
        pt = obj['portfolio']
        pt_addrs = obj['addresses']
        pt_user = pt.profile.user
        pt_asset_ids = [ ] # NOTE: keep track of asset_ids in pt to update list of assets user trusts

        # Only record portfolio value if user has registered at least one account
        if pt_addrs:
            # Get xlm_val added for each asset held in each account
            xlm_val = 0.0
            for acc, a in pt_addrs:
                try:
                    a.get()

                    acc_asset_ids = [ ] # NOTE; keep track of asset_ids in account to update list of assets this account trusts
                    for b in a.balances:
                        # Get the asset id and price for asset
                        asset_id = '{0}-{1}'.format(b['asset_code'], b['asset_issuer']) if b['asset_type'] != 'native' else 'XLM-native'

                        price = asset_prices.get(asset_id, 0.0) if b['asset_type'] != 'native' else 1.0

                        # Store the involved asset ids
                        acc_asset_ids.append(asset_id)
                        pt_asset_ids.append(asset_id)

                        # Update the total value
                        xlm_val += float(b['balance']) * price

                    # Update the list of assets this account trusts in db
                    acc.assets_trusting.clear()
                    acc.assets_trusting.add(*Asset.objects.filter(asset_id__in=acc_asset_ids))

                except AccountNotExistError:
                    # If it doesn't exist on the Stellar network, then remove account record from db
                    acc.delete()

            # Append to return iterable a dict of the data
            ret.append({ 'portfolio': pt, 'xlm_value': xlm_val, 'usd_value': xlm_val * usd_xlm_price })

        # Update the list of assets the user associated with the profile trusts in db
        pt_user.assets_trusting.clear()
        pt_user.assets_trusting.add(*Asset.objects.filter(asset_id__in=pt_asset_ids))

    return ret

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import os, requests, toml, urlparse

from allauth.utils import build_absolute_uri

from datetime import timedelta

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.db import models
from django.forms.models import model_to_dict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _

from functools import partial

from stellar_base.asset import Asset as StellarAsset
from stellar_base.address import Address
from stellar_base.utils import AccountNotExistError

from timeseries.utils import TimeSeriesModel, TimeSeriesManager

from . import managers, validators


def profile_file_directory_path(instance, filename, field):
    """
    File will be uploaded to MEDIA_ROOT/profile/<user_id>/<field>/<filename>
    """
    file_root, file_ext = os.path.splitext(filename)
    new_file_root = get_random_string().upper()
    new_filename = new_file_root + file_ext
    return 'profile/{0}/{1}/{2}'.format(instance.user_id, field, new_filename)

def model_file_directory_path(instance, filename, field):
    """
    File will be uploaded to MEDIA_ROOT/<model>/<id>/<field>/<filename>
    """
    file_root, file_ext = os.path.splitext(filename)
    new_file_root = get_random_string().upper()
    new_filename = new_file_root + file_ext
    return '{0}/{1}/{2}/{3}'.format(type(instance).__name__.lower(),
        instance.id, field, new_filename)

@python_2_unicode_compatible
class Profile(models.Model):
    """
    Profile associated with a user.
    """
    DEFAULT_PIC_URL = static('nc/images/profile.png')

    user = models.OneToOneField(settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE, related_name='profile', primary_key=True)
    bio = models.CharField(max_length=255, null=True, blank=True, default=None)
    pic = models.ImageField(
        _('Profile picture'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(profile_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )
    cover = models.ImageField(
        _('Cover photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(profile_file_directory_path, field='cover'),
        null=True, blank=True, default=None
    )
    followers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='profiles_following'
    )
    # NOTE: If private profile, must approve follower request for other user to
    # see assets, accounts, etc.
    is_private = models.BooleanField(default=False)
    accounts_created = models.PositiveSmallIntegerField(default=0)

    # Settings
    ## Email
    allow_payment_email = models.BooleanField(default=True)
    allow_token_issuance_email = models.BooleanField(default=True)
    allow_trade_email = models.BooleanField(default=True)
    allow_follower_email = models.BooleanField(default=True)
    allow_trust_email = models.BooleanField(default=True)

    # NOTE: user.get_full_name(), followers.count() are duplicated here
    # so Algolia search index updates work when user, follower updates occur (kept in sync through signals.py)
    full_name = models.CharField(max_length=200, null=True, blank=True, default=None)
    followers_count = models.IntegerField(default=0)

    def username(self):
        """
        Have this method as a proxy for the search index.

        user.username will never change in the db so search index related
        update issues with related fields don't exist here.
        """
        return self.user.username

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return reverse('nc:user-detail', kwargs={'slug': self.user.username})

    def __str__(self):
        bio =  ': ' + self.bio if self.bio else ''
        return self.user.username + bio


@python_2_unicode_compatible
class FollowRequest(models.Model):
    """
    Follow request for users who have a private profile.
    """
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='follower_requests', on_delete=models.CASCADE)
    requester = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='requests_to_follow', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('user', 'requester')

    def __str__(self):
        return 'Follow request: ' + self.requester.username + ' -> ' + self.user.username


@python_2_unicode_compatible
class Account(models.Model):
    """
    Store of Stellar public key addresses associated with user.
    """
    DEFAULT_PIC_URL = static('nc/images/account.png')

    public_key = models.CharField(max_length=56, unique=True)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='accounts', on_delete=models.CASCADE)
    name = models.CharField(_('Account name'), max_length=150,
        null=True, blank=True, default=None)
    pic = models.ImageField(
        _('Account photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )

    # StellarNotifier subscription id
    notifier_subscription_id = models.CharField(_('StellarNotifier subscription ID'), max_length=150,
        null=True, blank=True, default=None)

    # NOTE: user.get_full_name(), user.pic, profile_is_private duplicated here
    # so Algolia search index updates work when user updates occur (kept in sync through signals.py)
    user_full_name = models.CharField(max_length=200, null=True, blank=True, default=None)
    user_pic_url = models.URLField(null=True, blank=True, default=None)

    def create_notifier_subscription(self, request):
        """
        Create StellarNotifier subscription for this account
        and store the returned ID.

        NOTE: see https://github.com/orbitlens/stellar-notifier#create-subscription
        """
        # POST to the StellarNotifier subscription endpoint
        data = {
            'reaction_url': build_absolute_uri(request, reverse('nc:activity-create')),
            'account': self.public_key
        }
        print data
        r = requests.post(settings.STELLAR_NOTIFIER_SUBSCRIPTION_URL, data=data)

        # Store the returned notifier ID
        ret = {}
        if r.status_code == requests.codes.ok:
            ret = r.json()
            self.notifier_subscription_id = ret.get("id")
            self.save()

        # Return the response from notifier
        return ret

    def remove_notifier_subcription(self):
        """
        Remove StellarNotifier subscription for this account.

        NOTE: see https://github.com/orbitlens/stellar-notifier#remove-subscription
        """
        # DELETE to the StellarNotifier subscription endpoint
        # NOTE: StellarNotifier node js has unhandled promise when fetching subscriptions. WHY? Memory mode only?
        ret = {}
        if self.notifier_subscription_id:
            data = {
                'subscription_id': self.notifier_subscription_id
            }
            url = '{0}{1}'.format(settings.STELLAR_NOTIFIER_SUBSCRIPTION_URL, self.notifier_subscription_id)
            r = requests.delete(url, data=data)
            if r.status_code == requests.codes.ok:
                ret = r.json()

        # Return the response from notifier
        return ret

    def username(self):
        """
        Have this method as a proxy for the search index.

        user.username will never change in the db so search index related
        update issues with related fields don't exist here.
        """
        return self.user.username

    def profile_is_private(self):
        """
        Have this method as a proxy for the search index.

        Returns [0] if profile.is_private and [1] if not private.

        Need this to be a list of ints in order to work with OR bool
        in index.AccountIndex filters alongside viewable_by_if_private.
        """
        is_private_int = 0 if not self.user.profile.is_private else 1
        return [ is_private_int ]

    def viewable_by_if_private(self):
        """
        Have this method as a proxy for the search index to determine who
        can view this account if associated profile is private.

        If private, account can only be seen by those who follow this user's
        profile and the user themselves.

        Will have to call AccountIndex.save_record() if associated profile
        follower status changes. TODO: Use user change signals.
        """
        return [ u.id for u in self.user.profile.followers.all()\
            .union(get_user_model().objects.filter(id=self.user.id)) ]

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return '{}#accounts'.format(reverse('nc:user-detail', kwargs={'slug': self.user.username}))

    def __str__(self):
        name = self.name + ': ' if self.name else ''
        return name + self.public_key


@python_2_unicode_compatible
class AccountFundRequest(models.Model):
    """
    Requests for funding a new Stellar account.
    """
    public_key = models.CharField(max_length=56, unique=True)
    requester = models.ForeignKey(settings.AUTH_USER_MODEL,
        related_name='requests_to_fund_account', on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('public_key', 'requester')

    def __str__(self):
        return 'Funding request: ' + self.requester.username + ' -> ' + self.public_key


@python_2_unicode_compatible
class Asset(models.Model):
    """
    Store of Stellar public asset information so can customize image icon
    plus color scheme.

    If issuer is registered in Nucleo, allow them to change the color, pic
    of asset.
    """
    DEFAULT_PIC_URL = static('nc/images/asset.png')

    # NOTE: If None, then either doesn't have an account with us OR must be Lumens
    issuer = models.ForeignKey(Account, related_name='assets_issued',
        on_delete=models.CASCADE, null=True, blank=True, default=None)

    # NOTE: if None then must be Lumens: https://www.stellar.org/developers/guides/concepts/assets.html
    issuer_address = models.CharField(max_length=56, null=True, blank=True, default=None)
    domain = models.CharField(max_length=255, null=True, blank=True, default=None)
    code = models.CharField(max_length=12)
    asset_id = models.CharField(max_length=70, null=True, blank=True, default=None)

    trusters = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='assets_trusting'
    )

    # NOTE: Have both user and account trusters to differentiate after add/remove trust with button active state.
    account_trusters = models.ManyToManyField(
        Account,
        related_name='assets_trusting'
    )

    # NOTE: Since these meta fields are defined in the TOML (which Nucleo can't change
    # directly for logged in user clientside), should store them here to be safe
    name = models.CharField(max_length=255, null=True, blank=True, default=None)
    description = models.CharField(max_length=255, null=True, blank=True, default=None)
    conditions = models.CharField(max_length=255, null=True, blank=True, default=None)
    display_decimals = models.PositiveSmallIntegerField(default=7)

    pic = models.ImageField(
        _('Asset profile photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='pic'),
        null=True, blank=True, default=None
    )
    cover = models.ImageField(
        _('Asset cover photo'),
        validators=[validators.validate_file_size, validators.validate_image_mimetype],
        upload_to=partial(model_file_directory_path, field='cover'),
        null=True, blank=True, default=None
    )
    whitepaper = models.FileField(
        validators=[validators.validate_pdf_file_extension, validators.validate_file_size,
            validators.validate_pdf_mimetype],
        upload_to=partial(model_file_directory_path, field='whitepaper'),
        null=True, blank=True, default=None
    )

    toml = models.URLField(null=True, blank=True, default=None)
    toml_pic = models.URLField(null=True, blank=True, default=None)
    verified = models.BooleanField(default=False)

    # Asset manager
    objects = managers.AssetManager()

    def issuer_handle(self):
        """
        Have this method as a proxy for the search index.
        """
        if self.issuer:
            issuer_handle = '@' + self.issuer.user.username
        elif self.issuer_address:
            issuer_handle = None
        else:
            issuer_handle = 'Stellar Network'
        return issuer_handle

    def pic_url(self):
        """
        Have this method as a proxy for the search index.
        """
        pic_url = self.DEFAULT_PIC_URL
        if self.pic:
            pic_url = self.pic.url
        elif self.toml_pic:
            pic_url = self.toml_pic
        return pic_url

    def href(self):
        """
        Have this method as a proxy for the search index.
        """
        return reverse('nc:asset-detail', kwargs={'slug': self.asset_id})

    def type(self):
        """
        Have this method as a proxy for stellar asset type so don't have to
        rewrite guess() from stellar_base.
        """
        stellar_asset = StellarAsset(self.code, self.issuer_address)
        return stellar_asset.type

    def update_from_toml(self, toml_url=None):
        """
        Fetch toml file and update attributes of this instance with its details.
        """
        updated = False

        # Create dict of instance data to check for changes later
        fields_to_update = [ 'verified', 'toml', 'domain', 'toml_pic', 'name',
            'description', 'conditions', 'display_decimals' ]
        old_data = model_to_dict(self, fields=fields_to_update)

        # Set the new toml value
        self.toml = toml_url
        self.domain = urlparse.urlparse(toml_url).netloc if toml_url else None
        if not toml_url:
            self.verified = False

        # Fetch the toml file and check for this asset in [[CURRENCIES]]
        # Use try except in case no toml file exists at given toml_url
        if self.toml:
            try:
                r = requests.get(self.toml)
                parsed_toml = toml.loads(r.text)
                matched_currencies = [ c for c in parsed_toml.get('CURRENCIES', [])
                    if c.get('code', None) == self.code and c.get('issuer') == self.issuer_address ]

                self.verified = (len(matched_currencies) == 1)
                if self.verified:
                    # If matched, then asset has been verified and start updating instance fields
                    currency = matched_currencies[0]
                    if 'image' in currency:
                        self.toml_pic = currency['image']

                    if 'name' in currency:
                        # NOTE: problems here if desc is longer than 255 so concat for db
                        self.name = currency['name'][:255]

                    if 'desc' in currency:
                        # NOTE: problems here if desc is longer than 255 so concat for db
                        self.description = currency['desc'][:255]

                    if 'conditions' in currency:
                        # NOTE: problems here if conditions is longer than 255 so concat for db
                        self.conditions = currency['conditions'][:255]

                    if 'display_decimals' in currency:
                        self.display_decimals = currency['display_decimals']
            except requests.exceptions.ConnectionError:
                self.verified = False

        # Check whether any fields have been updated
        for field in fields_to_update:
            if old_data[field] != getattr(self, field):
                updated = True
                break

        # Save the instance
        if updated:
            self.save()

    class Meta:
        unique_together = ('issuer_address', 'code')

    def __str__(self):
        if self.asset_id:
            asset_id = self.asset_id
        elif self.issuer_address:
            asset_id = '{0}-{1}'.format(self.code, self.issuer_address)
        else:
            asset_id = '{0}-{1}'.format(instance.code, 'native')
        return asset_id


@python_2_unicode_compatible
class Portfolio(models.Model):
    """
    Stores time series data tracking cumulative value (in XLM) user has
    in all accounts associated with their user profile.
    """
    NOT_AVAILABLE = -1.0

    profile = models.OneToOneField(Profile,
        on_delete=models.CASCADE, related_name='portfolio', primary_key=True)

    # Most recent calculated balance values for easy access
    xlm_value = models.FloatField(default=NOT_AVAILABLE)
    usd_value = models.FloatField(default=NOT_AVAILABLE)

    # Performance stats for: 1d, 1w, 1m, 3m, 6m, 1y.
    # NOTE: Fractional values (i.e. need to mult by 100 to get percentages)
    performance_1d = models.FloatField(null=True, blank=True, default=None)
    performance_1w = models.FloatField(null=True, blank=True, default=None)
    performance_1m = models.FloatField(null=True, blank=True, default=None)
    performance_3m = models.FloatField(null=True, blank=True, default=None)
    performance_6m = models.FloatField(null=True, blank=True, default=None)
    performance_1y = models.FloatField(null=True, blank=True, default=None)

    # Rank is ordering of users for 1d performance. Only rank top 100
    rank = models.PositiveIntegerField(null=True, blank=True, default=None)

    # Portfolio manager
    objects = TimeSeriesManager()

    def __str__(self):
        return 'Portfolio: ' + self.profile.user.username


@python_2_unicode_compatible
class RawPortfolioData(TimeSeriesModel):
    # Update every 5 minutes on cron job but put min interval at 2.5 minutes to be safe.
    # NOTE: Cron job taking 1 min at this point with 100 portfolios + 100 assets
    # TODO: Parallelize cron workers for scaling by having separate instances take on smaller subsets of all portfolios?
    TIMESERIES_INTERVAL = timedelta(minutes=2.5)
    NOT_AVAILABLE = -1.0

    portfolio = models.ForeignKey(Portfolio, related_name='rawdata')
    xlm_value = models.FloatField(default=NOT_AVAILABLE)
    usd_value = models.FloatField(default=NOT_AVAILABLE)

    def __str__(self):
        return str(self.portfolio) + ': ' + str(self.usd_value) + ' (' + str(self.created) + ')'


def portfolio_data_collector(queryset, asset_prices):
    """
    Should return an iterable that yields dictionaries of data
    needed to successfully create a RawPortfolioData instance.

    asset_prices is a dict with asset_id: current_market_price, with
    market prices as float.
    If not native, market prices in XLM. Otherwise, in USD.
    """
    # Store the xlm price in USD from asset_prices dict
    usd_xlm_price = asset_prices['XLM-native']

    # Map from model_asset.asset_id to model_asset.id
    # TODO: make model_asset.asset_id unique key
    model_asset_map = {
        asset.asset_id: asset.id
        for asset in Asset.objects.all()
    }

    # Accumulate stellar addresses for each user
    portfolio_addresses = [
        {
            'portfolio': pt,
            'addresses': [
                (a, Address(address=a.public_key, network=settings.STELLAR_NETWORK))
                for a in pt.profile.user.accounts.all()
            ]
        }
        for pt in queryset.prefetch_related('profile__user__accounts')
    ]

    # Retrieve addresses from Horizon then calculate portfolio value
    # given asset balances
    ret = []
    for obj in portfolio_addresses:
        pt = obj['portfolio']
        pt_addrs = obj['addresses']
        pt_user = pt.profile.user
        pt_asset_ids = [ ] # NOTE: keep track of asset_ids in pt to update list of assets user trusts

        # Only record portfolio value if user has registered at least one account
        if pt_addrs:
            # Get xlm_val added for each asset held in each account
            xlm_val = 0.0
            for acc, a in pt_addrs:
                try:
                    a.get()

                    acc_asset_ids = [ ] # NOTE; keep track of asset_ids in account to update list of assets this account trusts
                    for b in a.balances:
                        # Get the asset id and price for asset
                        asset_id = '{0}-{1}'.format(b['asset_code'], b['asset_issuer']) if b['asset_type'] != 'native' else 'XLM-native'

                        price = asset_prices.get(asset_id, 0.0) if b['asset_type'] != 'native' else 1.0

                        # Store the involved asset ids
                        acc_asset_ids.append(asset_id)
                        pt_asset_ids.append(asset_id)

                        # Update the total value
                        xlm_val += float(b['balance']) * price

                    # Update the list of assets this account trusts in db
                    acc.assets_trusting.clear()
                    acc.assets_trusting.add(*Asset.objects.filter(asset_id__in=acc_asset_ids))

                except AccountNotExistError:
                    # If it doesn't exist on the Stellar network, then remove account record from db
                    acc.delete()

            # Append to return iterable a dict of the data
            ret.append({ 'portfolio': pt, 'xlm_value': xlm_val, 'usd_value': xlm_val * usd_xlm_price })

        # Update the list of assets the user associated with the profile trusts in db
        pt_user.assets_trusting.clear()
        pt_user.assets_trusting.add(*Asset.objects.filter(asset_id__in=pt_asset_ids))

    return ret
# -*- coding: utf-8 -*- File will be uploaded to MEDIA_ROOT/profile/<user_id>/<field>/<filename> File will be uploaded to MEDIA_ROOT/<model>/<id>/<field>/<filename> Profile associated with a user. # NOTE: If private profile, must approve follower request for other user to # see assets, accounts, etc. # Settings ## Email # NOTE: user.get_full_name(), followers.count() are duplicated here # so Algolia search index updates work when user, follower updates occur (kept in sync through signals.py) Have this method as a proxy for the search index.          user.username will never change in the db so search index related         update issues with related fields don't exist here. Have this method as a proxy for the search index. Have this method as a proxy for the search index. Follow request for users who have a private profile. Store of Stellar public key addresses associated with user. # StellarNotifier subscription id # NOTE: user.get_full_name(), user.pic, profile_is_private duplicated here # so Algolia search index updates work when user updates occur (kept in sync through signals.py) Create StellarNotifier subscription for this account         and store the returned ID.          NOTE: see https://github.com/orbitlens/stellar-notifier#create-subscription # POST to the StellarNotifier subscription endpoint # Store the returned notifier ID # Return the response from notifier Remove StellarNotifier subscription for this account.          NOTE: see https://github.com/orbitlens/stellar-notifier#remove-subscription # DELETE to the StellarNotifier subscription endpoint # NOTE: StellarNotifier node js has unhandled promise when fetching subscriptions. WHY? Memory mode only? # Return the response from notifier Have this method as a proxy for the search index.          user.username will never change in the db so search index related         update issues with related fields don't exist here. Have this method as a proxy for the search index.          Returns [0] if profile.is_private and [1] if not private.          Need this to be a list of ints in order to work with OR bool         in index.AccountIndex filters alongside viewable_by_if_private. Have this method as a proxy for the search index to determine who         can view this account if associated profile is private.          If private, account can only be seen by those who follow this user's         profile and the user themselves.          Will have to call AccountIndex.save_record() if associated profile         follower status changes. TODO: Use user change signals. Have this method as a proxy for the search index. Have this method as a proxy for the search index. #accounts'.format(reverse('nc:user-detail', kwargs={'slug': self.user.username})) Requests for funding a new Stellar account. Store of Stellar public asset information so can customize image icon     plus color scheme.      If issuer is registered in Nucleo, allow them to change the color, pic     of asset. # NOTE: If None, then either doesn't have an account with us OR must be Lumens # NOTE: if None then must be Lumens: https://www.stellar.org/developers/guides/concepts/assets.html # NOTE: Have both user and account trusters to differentiate after add/remove trust with button active state. # NOTE: Since these meta fields are defined in the TOML (which Nucleo can't change # directly for logged in user clientside), should store them here to be safe # Asset manager Have this method as a proxy for the search index. Have this method as a proxy for the search index. Have this method as a proxy for the search index. Have this method as a proxy for stellar asset type so don't have to         rewrite guess() from stellar_base. Fetch toml file and update attributes of this instance with its details. # Create dict of instance data to check for changes later # Set the new toml value # Fetch the toml file and check for this asset in [[CURRENCIES]] # Use try except in case no toml file exists at given toml_url # If matched, then asset has been verified and start updating instance fields # NOTE: problems here if desc is longer than 255 so concat for db # NOTE: problems here if desc is longer than 255 so concat for db # NOTE: problems here if conditions is longer than 255 so concat for db # Check whether any fields have been updated # Save the instance Stores time series data tracking cumulative value (in XLM) user has     in all accounts associated with their user profile. # Most recent calculated balance values for easy access # Performance stats for: 1d, 1w, 1m, 3m, 6m, 1y. # NOTE: Fractional values (i.e. need to mult by 100 to get percentages) # Rank is ordering of users for 1d performance. Only rank top 100 # Portfolio manager # Update every 5 minutes on cron job but put min interval at 2.5 minutes to be safe. # NOTE: Cron job taking 1 min at this point with 100 portfolios + 100 assets # TODO: Parallelize cron workers for scaling by having separate instances take on smaller subsets of all portfolios? Should return an iterable that yields dictionaries of data     needed to successfully create a RawPortfolioData instance.      asset_prices is a dict with asset_id: current_market_price, with     market prices as float.     If not native, market prices in XLM. Otherwise, in USD. # Store the xlm price in USD from asset_prices dict # Map from model_asset.asset_id to model_asset.id # TODO: make model_asset.asset_id unique key # Accumulate stellar addresses for each user # Retrieve addresses from Horizon then calculate portfolio value # given asset balances # NOTE: keep track of asset_ids in pt to update list of assets user trusts # Only record portfolio value if user has registered at least one account # Get xlm_val added for each asset held in each account # NOTE; keep track of asset_ids in account to update list of assets this account trusts # Get the asset id and price for asset # Store the involved asset ids # Update the total value # Update the list of assets this account trusts in db # If it doesn't exist on the Stellar network, then remove account record from db # Append to return iterable a dict of the data # Update the list of assets the user associated with the profile trusts in db