<commit_before>from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe

from projects import constants
from projects.models import Project, EmailHook
from projects.tasks import update_docs

class ProjectForm(forms.ModelForm):
    required_css_class = "required"

    def clean_name(self):
        name = self.cleaned_data.get('name', '')
        if not self.instance.pk:
            potential_slug = slugify(name)
            if Project.objects.filter(slug=potential_slug).count():
                raise forms.ValidationError(
                    _('A project with that name exists already!')
                )

        return name

class ImportProjectForm(ProjectForm):
    repo = forms.CharField(required=True,
                           help_text=_(u'URL for your code (hg or git). Ex. '
                                       u'http://github.com/ericholscher/django'
                                       u'-kong.git'))

    python_interpreter = forms.ChoiceField(
        choices=constants.PYTHON_CHOICES, initial='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    class Meta:
        model = Project
        fields = (
            # Important
            'name', 'repo', 'repo_type', 'description', 'language',
            # Not as important
            'project_url', 'tags', 'default_branch', 'default_version',
            'conf_py_file',
            # Privacy
            # 'privacy_level', 'version_privacy_level',
            # Python specific
            'use_virtualenv', 'use_system_packages', 'requirements_file',
            'python_interpreter',
            # Fringe
            'analytics_code', 'documentation_type', 'tags'
        )

    def clean_repo(self):
        repo = self.cleaned_data.get('repo', '').strip()
        pvt_repos = getattr(settings, 'ALLOW_PRIVATE_REPOS', False)
        if '&&' in repo or '|' in repo:
            raise forms.ValidationError(_(u'Invalid character in repo name'))
        elif '@' in repo and not pvt_repos:
            raise forms.ValidationError(
                _(u'It looks like you entered a private repo - please use the '
                  u'public (http:// or git://) clone url'))
        return repo

    def clean_conf_py_file(self):
        file = self.cleaned_data.get('conf_py_file', '').strip()
        if file and not 'conf.py' in file:
            raise forms.ValidationError(
                _('Your configuration file is invalid, make sure it contains '
                  'conf.py in it.'))
        return file

    def save(self, *args, **kwargs):
        # save the project
        project = super(ImportProjectForm, self).save(*args, **kwargs)

        # kick off the celery job
        update_docs.delay(pk=project.pk)

        return project

class DualCheckboxWidget(forms.CheckboxInput):
    def __init__(self, version, attrs=None, check_test=bool):
        super(DualCheckboxWidget, self).__init__(attrs, check_test)
        self.version = version

    def render(self, name, value, attrs=None):
        checkbox = super(DualCheckboxWidget, self).render(name, value, attrs)
        icon = self.render_icon()
        return mark_safe(u'%s%s' % (checkbox, icon))

    def render_icon(self):
        context = {
            'MEDIA_URL': settings.MEDIA_URL,
            'built': self.version.built,
            'uploaded': self.version.uploaded,
            'url': self.version.get_absolute_url()
        }
        return render_to_string('projects/includes/icon_built.html', context)

class BaseVersionsForm(forms.Form):
    def save(self):
        versions = self.project.versions.all()
        for version in versions:
            self.save_version(version)
        default_version = self.cleaned_data.get('default-version', None)
        if default_version:
            self.project.default_version = default_version
            self.project.save()

    def save_version(self, version):
        new_value = self.cleaned_data.get('version-%s' % version.slug, None)
        privacy_level = self.cleaned_data.get('privacy-%s' % version.slug,
                                              None)
        if ((new_value is None or
             new_value == version.active)
            and (privacy_level is None or
                 privacy_level == version.privacy_level)):
            return
        version.active = new_value
        version.privacy_level = privacy_level
        version.save()
        if version.active and not version.built and not version.uploaded:
            update_docs.delay(self.project.pk, record=True,
                              version_pk=version.pk)

def build_versions_form(project):
    attrs = {
        'project': project,
    }
    versions_qs = project.versions.all()
    active = versions_qs.filter(active=True)
    if active.exists():
        choices = [(version.slug, version.verbose_name) for version in active]
        attrs['default-version'] = forms.ChoiceField(
            label=_("Default Version"),
            choices=choices,
            initial=project.get_default_version(),
        )
    for version in versions_qs:
        field_name = 'version-%s' % version.slug
        privacy_name = 'privacy-%s' % version.slug
        attrs[field_name] = forms.BooleanField(
            label=version.verbose_name,
            widget=DualCheckboxWidget(version),
            initial=version.active,
            required=False,
        )
        attrs[privacy_name] = forms.ChoiceField(
            # This isn't a real label, but just a slug for the template
            label="privacy",
            choices=constants.PRIVACY_CHOICES,
            initial=version.privacy_level,
        )
    return type('VersionsForm', (BaseVersionsForm,), attrs)

class BaseUploadHTMLForm(forms.Form):
    content = forms.FileField(label=_("Zip file of HTML"))
    overwrite = forms.BooleanField(required=False,
                                   label=_("Overwrite existing HTML?"))

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(BaseUploadHTMLForm, self).__init__(*args, **kwargs)

    def clean(self):
        version_slug = self.cleaned_data['version']
        file = self.request.FILES['content']
        version = self.project.versions.get(slug=version_slug)

        #Validation
        if version.active and not self.cleaned_data.get('overwrite', False):
            raise forms.ValidationError(_("That version is already active!"))
        if not file.name.endswith('zip'):
            raise forms.ValidationError(_("Must upload a zip file."))

        return self.cleaned_data

def build_upload_html_form(project):
    attrs = {
        'project': project,
    }
    active = project.versions.all()
    if active.exists():
        choices = []
        choices += [(version.slug, version.verbose_name) for version in active]
        attrs['version'] = forms.ChoiceField(
            label=_("Version of the project you are uploading HTML for"),
            choices=choices,
        )
    return type('UploadHTMLForm', (BaseUploadHTMLForm,), attrs)

class SubprojectForm(forms.Form):
    subproject = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.parent = kwargs.pop('parent', None)
        super(SubprojectForm, self).__init__(*args, **kwargs)

    def clean_subproject(self):
        subproject_name = self.cleaned_data['subproject']
        subproject_qs = Project.objects.filter(name=subproject_name)
        if not subproject_qs.exists():
            raise forms.ValidationError((_("Project %(name)s does not exist")
                                         % {'name': subproject_name}))
        self.subproject = subproject_qs[0]
        return subproject_name

    def save(self):
        relationship = self.parent.add_subproject(self.subproject)
        return relationship

class UserForm(forms.Form):
    user = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.project = kwargs.pop('project', None)
        super(UserForm, self).__init__(*args, **kwargs)

    def clean_user(self):
        name = self.cleaned_data['user']
        user_qs = User.objects.filter(username=name)
        if not user_qs.exists():
            raise forms.ValidationError(_("User %(name)s does not exist") %
                                        {'name': name})
        self.user = user_qs[0]
        return name

    def save(self):
        self.project.users.add(self.user)
        return self.user

class EmailHookForm(forms.Form):
    email = forms.EmailField()

    def __init__(self, *args, **kwargs):
        self.project = kwargs.pop('project', None)
        super(EmailHookForm, self).__init__(*args, **kwargs)

    def clean_email(self):
        self.email = EmailHook.objects.get_or_create(
            email=self.cleaned_data['email'], project=self.project)[0]
        return self.email

    def save(self):
        self.project.emailhook_notifications.add(self.email)
        return self.project

class TranslationForm(forms.Form):
    project = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.parent = kwargs.pop('parent', None)
        super(TranslationForm, self).__init__(*args, **kwargs)

    def clean_project(self):
        subproject_name = self.cleaned_data['project']
        subproject_qs = Project.objects.filter(name=subproject_name)
        if not subproject_qs.exists():
            raise forms.ValidationError((_("Project %(name)s does not exist")
                                         % {'name': subproject_name}))
        self.subproject = subproject_qs[0]
        return subproject_name

    def save(self):
        project = self.parent.translations.add(self.subproject)
        return project
<commit_msg>derp, privacy levels are fine too<commit_after>from django import forms
from django.conf import settings
from django.contrib.auth.models import User
from django.template.defaultfilters import slugify
from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe

from projects import constants
from projects.models import Project, EmailHook
from projects.tasks import update_docs

class ProjectForm(forms.ModelForm):
    required_css_class = "required"

    def clean_name(self):
        name = self.cleaned_data.get('name', '')
        if not self.instance.pk:
            potential_slug = slugify(name)
            if Project.objects.filter(slug=potential_slug).count():
                raise forms.ValidationError(
                    _('A project with that name exists already!')
                )

        return name

class ImportProjectForm(ProjectForm):
    repo = forms.CharField(required=True,
                           help_text=_(u'URL for your code (hg or git). Ex. '
                                       u'http://github.com/ericholscher/django'
                                       u'-kong.git'))

    python_interpreter = forms.ChoiceField(
        choices=constants.PYTHON_CHOICES, initial='python',
        help_text=_("(Beta) The Python interpreter used to create the virtual "
                    "environment."))

    class Meta:
        model = Project
        fields = (
            # Important
            'name', 'repo', 'repo_type', 'description', 'language',
            # Not as important
            'project_url', 'tags', 'default_branch', 'default_version',
            'conf_py_file',
            # Privacy
            'privacy_level', 'version_privacy_level',
            # Python specific
            'use_virtualenv', 'use_system_packages', 'requirements_file',
            'python_interpreter',
            # Fringe
            'analytics_code', 'documentation_type', 'tags'
        )

    def clean_repo(self):
        repo = self.cleaned_data.get('repo', '').strip()
        pvt_repos = getattr(settings, 'ALLOW_PRIVATE_REPOS', False)
        if '&&' in repo or '|' in repo:
            raise forms.ValidationError(_(u'Invalid character in repo name'))
        elif '@' in repo and not pvt_repos:
            raise forms.ValidationError(
                _(u'It looks like you entered a private repo - please use the '
                  u'public (http:// or git://) clone url'))
        return repo

    def clean_conf_py_file(self):
        file = self.cleaned_data.get('conf_py_file', '').strip()
        if file and not 'conf.py' in file:
            raise forms.ValidationError(
                _('Your configuration file is invalid, make sure it contains '
                  'conf.py in it.'))
        return file

    def save(self, *args, **kwargs):
        # save the project
        project = super(ImportProjectForm, self).save(*args, **kwargs)

        # kick off the celery job
        update_docs.delay(pk=project.pk)

        return project

class DualCheckboxWidget(forms.CheckboxInput):
    def __init__(self, version, attrs=None, check_test=bool):
        super(DualCheckboxWidget, self).__init__(attrs, check_test)
        self.version = version

    def render(self, name, value, attrs=None):
        checkbox = super(DualCheckboxWidget, self).render(name, value, attrs)
        icon = self.render_icon()
        return mark_safe(u'%s%s' % (checkbox, icon))

    def render_icon(self):
        context = {
            'MEDIA_URL': settings.MEDIA_URL,
            'built': self.version.built,
            'uploaded': self.version.uploaded,
            'url': self.version.get_absolute_url()
        }
        return render_to_string('projects/includes/icon_built.html', context)

class BaseVersionsForm(forms.Form):
    def save(self):
        versions = self.project.versions.all()
        for version in versions:
            self.save_version(version)
        default_version = self.cleaned_data.get('default-version', None)
        if default_version:
            self.project.default_version = default_version
            self.project.save()

    def save_version(self, version):
        new_value = self.cleaned_data.get('version-%s' % version.slug, None)
        privacy_level = self.cleaned_data.get('privacy-%s' % version.slug,
                                              None)
        if ((new_value is None or
             new_value == version.active)
            and (privacy_level is None or
                 privacy_level == version.privacy_level)):
            return
        version.active = new_value
        version.privacy_level = privacy_level
        version.save()
        if version.active and not version.built and not version.uploaded:
            update_docs.delay(self.project.pk, record=True,
                              version_pk=version.pk)

def build_versions_form(project):
    attrs = {
        'project': project,
    }
    versions_qs = project.versions.all()
    active = versions_qs.filter(active=True)
    if active.exists():
        choices = [(version.slug, version.verbose_name) for version in active]
        attrs['default-version'] = forms.ChoiceField(
            label=_("Default Version"),
            choices=choices,
            initial=project.get_default_version(),
        )
    for version in versions_qs:
        field_name = 'version-%s' % version.slug
        privacy_name = 'privacy-%s' % version.slug
        attrs[field_name] = forms.BooleanField(
            label=version.verbose_name,
            widget=DualCheckboxWidget(version),
            initial=version.active,
            required=False,
        )
        attrs[privacy_name] = forms.ChoiceField(
            # This isn't a real label, but just a slug for the template
            label="privacy",
            choices=constants.PRIVACY_CHOICES,
            initial=version.privacy_level,
        )
    return type('VersionsForm', (BaseVersionsForm,), attrs)

class BaseUploadHTMLForm(forms.Form):
    content = forms.FileField(label=_("Zip file of HTML"))
    overwrite = forms.BooleanField(required=False,
                                   label=_("Overwrite existing HTML?"))

    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop('request', None)
        super(BaseUploadHTMLForm, self).__init__(*args, **kwargs)

    def clean(self):
        version_slug = self.cleaned_data['version']
        file = self.request.FILES['content']
        version = self.project.versions.get(slug=version_slug)

        #Validation
        if version.active and not self.cleaned_data.get('overwrite', False):
            raise forms.ValidationError(_("That version is already active!"))
        if not file.name.endswith('zip'):
            raise forms.ValidationError(_("Must upload a zip file."))

        return self.cleaned_data

def build_upload_html_form(project):
    attrs = {
        'project': project,
    }
    active = project.versions.all()
    if active.exists():
        choices = []
        choices += [(version.slug, version.verbose_name) for version in active]
        attrs['version'] = forms.ChoiceField(
            label=_("Version of the project you are uploading HTML for"),
            choices=choices,
        )
    return type('UploadHTMLForm', (BaseUploadHTMLForm,), attrs)

class SubprojectForm(forms.Form):
    subproject = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.parent = kwargs.pop('parent', None)
        super(SubprojectForm, self).__init__(*args, **kwargs)

    def clean_subproject(self):
        subproject_name = self.cleaned_data['subproject']
        subproject_qs = Project.objects.filter(name=subproject_name)
        if not subproject_qs.exists():
            raise forms.ValidationError((_("Project %(name)s does not exist")
                                         % {'name': subproject_name}))
        self.subproject = subproject_qs[0]
        return subproject_name

    def save(self):
        relationship = self.parent.add_subproject(self.subproject)
        return relationship

class UserForm(forms.Form):
    user = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.project = kwargs.pop('project', None)
        super(UserForm, self).__init__(*args, **kwargs)

    def clean_user(self):
        name = self.cleaned_data['user']
        user_qs = User.objects.filter(username=name)
        if not user_qs.exists():
            raise forms.ValidationError(_("User %(name)s does not exist") %
                                        {'name': name})
        self.user = user_qs[0]
        return name

    def save(self):
        self.project.users.add(self.user)
        return self.user

class EmailHookForm(forms.Form):
    email = forms.EmailField()

    def __init__(self, *args, **kwargs):
        self.project = kwargs.pop('project', None)
        super(EmailHookForm, self).__init__(*args, **kwargs)

    def clean_email(self):
        self.email = EmailHook.objects.get_or_create(
            email=self.cleaned_data['email'], project=self.project)[0]
        return self.email

    def save(self):
        self.project.emailhook_notifications.add(self.email)
        return self.project

class TranslationForm(forms.Form):
    project = forms.CharField()

    def __init__(self, *args, **kwargs):
        self.parent = kwargs.pop('parent', None)
        super(TranslationForm, self).__init__(*args, **kwargs)

    def clean_project(self):
        subproject_name = self.cleaned_data['project']
        subproject_qs = Project.objects.filter(name=subproject_name)
        if not subproject_qs.exists():
            raise forms.ValidationError((_("Project %(name)s does not exist")
                                         % {'name': subproject_name}))
        self.subproject = subproject_qs[0]
        return subproject_name

    def save(self):
        project = self.parent.translations.add(self.subproject)
        return project
<|endoftext|>


<commit_before># -*- coding:utf-8 -*-
import time

import rospy
from bitbots_cm730.srv import SwitchMotorPower
from humanoid_league_msgs.msg import RobotControlState

from .abstract_state_machine import AbstractState, VALUES
from .abstract_state_machine import AbstractStateMachine
from humanoid_league_speaker.speaker import speak
from humanoid_league_msgs.msg import Speak

STATE_CONTROLABLE = 0
STATE_FALLING = 1
STATE_FALLEN = 2
STATE_GETTING_UP = 3
STATE_ANIMATION_RUNNING = 4
STATE_STARTUP = 5
STATE_SHUT_DOWN = 6
STATE_PENALTY = 7
STATE_PENALTY_ANIMATION = 8
STATE_RECORD = 9
STATE_WALKING = 10

class HcmStateMachine(AbstractStateMachine):
    def __init__(self, die_flag, standup_flag, soft_off_flag, soft_start, start_test, state_publisher):
        super(HcmStateMachine, self).__init__()
        VALUES.standup_flag = standup_flag
        VALUES.die_flag = die_flag
        VALUES.soft_off_flag = soft_off_flag
        VALUES.soft_start = soft_start
        VALUES.start_test = start_test
        VALUES.last_request = 0
        VALUES.last_hardware_update = None

        self.error_state = ShutDown()
        self.state_publisher = state_publisher

        self.connections = rospy.get_param("hcm_state_machine")

        self.set_state(Startup())

    def publish_state(self, state):
        msg = RobotControlState()
        msg.state = state.hcm_state()
        self.state_publisher.publish(msg)

    def is_penalized(self):
        return self.state.__class__ in (Penalty, PenaltyAnimationIn, PenaltyAnimationOut)

    def is_walking(self):
        return self.state.__class__ in (Walking, WalkingStopping)

    def is_record(self):
        return self.state.__class__ is Record

    def is_shutdown(self):
        return self.state.__class__ is ShutDown

class Startup(AbstractState):
    def entry(self):
        self.start_time_limit = rospy.get_param("hcm/start_time", 10)

    def evaluate(self):
        if not self.animation_started:
            # leave this if we got a hardware response, or after some time
            if VALUES.last_hardware_update is not None or time.time() - VALUES.start_up_time > self.start_time_limit:
                # check if we directly go into a special state, if not, got to get up
                if VALUES.start_test:
                    pass
                    return
                if VALUES.record:
                    switch_motor_power(True)
                    return Record()
                if VALUES.soft_start:
                    switch_motor_power(False)
                    # to prohibit getting directly out of softoff
                    VALUES.last_client_update = time.time() - 120
                    return Softoff()
                switch_motor_power(True)
                if VALUES.penalized:
                    return PenaltyAnimationIn()

                # normal states
                if VALUES.standup_flag:
                    self.next_state = GettingUp()
                else:
                    self.next_state = Controllable()
                # play init
                self.start_animation(rospy.get_param("hcm/animations/init"))
            else:
                rospy.loginfo_throttle(1, "Motion is waiting for data from hardware")
        else:
            if self.animation_finished():
                return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_STARTUP

    def shutdown(self):
        return ShutDown()

class Softoff(AbstractState):
    def entry(self):
        switch_motor_power(False)

    def evaluate(self):
        if not self.animation_started:
            if VALUES.record:
                switch_motor_power(True)
                self.next_state = Record()
                # don't directly change state, we wait for animation to finish
                self.start_animation(rospy.get_param("hcm/animations/walkready"))
                return
            if time.time() - VALUES.last_request < 10:
                # got a new move request
                switch_motor_power(True)
                self.next_state = Controllable()
                self.start_animation(rospy.get_param("hcm/animations/walkready"))
                return
            if VALUES.is_die_time():
                return ShutDown()
            if VALUES.external_animation_requested:
                switch_motor_power(True)
                return Controllable()
            rospy.sleep(0.1)
        else:
            if self.animation_finished():
                return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        # we want to look controlable to the outside
        return STATE_CONTROLABLE

    def shutdown(self):
        return ShutDown()

class Record(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if not VALUES.record:
            return Controllable()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_RECORD

    def shutdown(self):
        return ShutDownAnimation()

class PenaltyAnimationIn(AbstractState):
    def entry(self):
        rospy.logwarn("Penalized, sitting down!")
        self.next_state = Penalty()
        self.start_animation(rospy.get_param("hcm/animations/penalized"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_PENALTY_ANIMATION

    def shutdown(self):
        return ShutDownAnimation()

class PenaltyAnimationOut(AbstractState):
    def entry(self):
        rospy.logwarn("Not penalized anymore, getting up!")
        self.next_state = Controllable()
        self.start_animation(rospy.get_param("hcm/animations/penalized_end"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_PENALTY_ANIMATION

    def shutdown(self):
        return ShutDownAnimation()

class Penalty(AbstractState):
    def entry(self):
        switch_motor_power(False)
        rospy.loginfo("Im now penalized")

    def evaluate(self):
        if VALUES.penalized:
            # still penalized, lets wait a bit
            rospy.sleep(0.05)
            # prohibit soft off
            VALUES.last_client_update = time.time()
        else:
            return PenaltyAnimationOut()

    def exit(self):
        switch_motor_power(True)

    def hcm_state(self):
        return STATE_PENALTY

    def shutdown(self):
        return ShutDown()

class GettingUp(AbstractState):
    """This state starts the getting up procedure. """

    def entry(self):
        # normally we should got to getting up second after this
        self.next_state = GettingUpSecond()
        # but lets check if we actually have to stand up
        fallen = VALUES.fall_checker.check_fallen(VALUES.smooth_accel)
        if fallen is not None:
            self.start_animation(fallen)
        else:
            self.next_state = Controllable()
            self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            # head on
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_GETTING_UP

    def shutdown(self):
        return ShutDownAnimation()

class GettingUpSecond(AbstractState):
    """This state plays a second animation for getting up."""

    def entry(self):
        self.next_state = Controllable()
        self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        if self.animation_finished():
            # head on
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_GETTING_UP

    def shutdown(self):
        return ShutDownAnimation()

class Controllable(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if VALUES.record:
            return Record()
        if VALUES.penalized:
            return PenaltyAnimationIn()
        if VALUES.is_soft_off_time():
            return Softoff()
        if VALUES.is_die_time():
            rospy.logwarn("die time")
            return ShutDownAnimation()

        if VALUES.standup_flag:
            ## Falling detection
            falling_pose = VALUES.fall_checker.check_falling(VALUES.not_so_smooth_gyro)
            if falling_pose is not None:
                return Falling()
            ### Standing up
            direction_animation = VALUES.fall_checker.check_fallen(VALUES.smooth_accel)
            if direction_animation is not None:
                return Fallen()

        if VALUES.external_animation_playing:
            return AnimationRunning()

        if VALUES.walking_active:
            return Walking()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_CONTROLABLE

    def shutdown(self):
        return ShutDownAnimation()

class Falling(AbstractState):
    def entry(self):
        self.wait_time = time.time()
        # go directly in falling pose
        falling_pose = VALUES.fall_checker.check_falling(VALUES.not_so_smooth_gyro)
        if falling_pose is not None:
            # we're falling, stay in falling
            self.next_state = Falling()
            self.start_animation(falling_pose)
        else:
            # we're not falling anymore
            if VALUES.is_fallen():
                # go directly to fallen
                return Fallen()
            else:
                self.next_state = Controllable()
                self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        # we wait a moment before going to the next state
        if time.time() - self.wait_time > 3 and self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_FALLING

    def shutdown(self):
        return ShutDown()

class Fallen(AbstractState):
    def entry(self):
        direction_animation = VALUES.is_fallen()
        if direction_animation is None:
            # we don't have to stand up, go directly to controllable
            return Controllable()
        else:
            # do corresponding animation
            self.next_state = GettingUp()
            self.start_animation(direction_animation)

    def evaluate(self):
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_FALLEN

    def shutdown(self):
        return ShutDown()

class Walking(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if VALUES.record:
            return Record()
        if VALUES.penalized:
            return PenaltyAnimationIn()
        if VALUES.is_soft_off_time():
            return Softoff()
        if VALUES.is_die_time():
            return ShutDownAnimation()
        if VALUES.standup_flag:
            # Falling detection
            if VALUES.is_falling():
                return Falling()
            # Standing up
            if VALUES.is_fallen():
                return Fallen()

        if VALUES.external_animation_requested:
            return WalkingStopping()

        if not VALUES.walking_active:
            return Controllable()

    def exit(self):
        VALUES.walking_active = False

    def hcm_state(self):
        return STATE_WALKING

    def shutdown(self):
        return ShutDownAnimation()

class WalkingStopping(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        return Controllable()

    def exit(self):
        VALUES.walking_active = False

    def hcm_state(self):
        return STATE_WALKING

    def shutdown(self):
        return ShutDownAnimation()

class AnimationRunning(AbstractState):
    def entry(self):
        # reset flags
        VALUES.external_animation_playing = False
        VALUES.external_animation_requested = False

    def evaluate(self):
        # if external animation finished
        if not VALUES.external_animation_finished:
            return Controllable()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_ANIMATION_RUNNING

    def shutdown(self):
        return ShutDownAnimation()

class ShutDownAnimation(AbstractState):
    def entry(self):
        rospy.loginfo("Motion will shut off")
        speak("Motion will shut off", VALUES.speak_publisher, priority=Speak.HIGH_PRIORITY)
        self.start_animation(rospy.get_param("hcm/animations/shut-down"))

    def evaluate(self):
        if self.animation_finished():
            return ShutDown()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_SHUT_DOWN

    def shutdown(self):
        # we are shutting down, don't change
        return None

class ShutDown(AbstractState):
    def entry(self):
        speak("Motion says good bye", VALUES.speak_publisher, priority=Speak.HIGH_PRIORITY)
        # give humans the chance to hold the robot before turning it off
        rospy.sleep(3)
        switch_motor_power(False)
        rospy.loginfo("Motion is now off, good bye")

    def evaluate(self):
        # do nothing, the state machine will see that we're finished
        pass

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_SHUT_DOWN

    def shutdown(self):
        # this is shut down
        return None

def switch_motor_power(state):
    """ Calling service from CM730 to turn motor power on or off. But only if not using simulator"""
    if rospy.get_param("simulation_active", False):
        rospy.loginfo("I'm simulating, not switching motorpower to " + state.__str__())
    else:
        try:
            rospy.wait_for_service("switch_motor_power", timeout=1)
        except rospy.ROSException:
            rospy.logfatal("Can't switch of motorpower, seems like the CM730 is missing.")
            return
        power_switch = rospy.ServiceProxy("switch_motor_power", SwitchMotorPower)
        try:
            response = power_switch(state)
        except rospy.ServiceException as exc:
            print("Service did not process request: " + str(exc))
        # wait for motors
        rospy.sleep(1)
<commit_msg>replaced time.time with rospy.get_time for simulataion etc<commit_after># -*- coding:utf-8 -*-
import time

import rospy
from bitbots_cm730.srv import SwitchMotorPower
from humanoid_league_msgs.msg import RobotControlState

from .abstract_state_machine import AbstractState, VALUES
from .abstract_state_machine import AbstractStateMachine
from humanoid_league_speaker.speaker import speak
from humanoid_league_msgs.msg import Speak

STATE_CONTROLABLE = 0
STATE_FALLING = 1
STATE_FALLEN = 2
STATE_GETTING_UP = 3
STATE_ANIMATION_RUNNING = 4
STATE_STARTUP = 5
STATE_SHUT_DOWN = 6
STATE_PENALTY = 7
STATE_PENALTY_ANIMATION = 8
STATE_RECORD = 9
STATE_WALKING = 10

class HcmStateMachine(AbstractStateMachine):
    def __init__(self, die_flag, standup_flag, soft_off_flag, soft_start, start_test, state_publisher):
        super(HcmStateMachine, self).__init__()
        VALUES.standup_flag = standup_flag
        VALUES.die_flag = die_flag
        VALUES.soft_off_flag = soft_off_flag
        VALUES.soft_start = soft_start
        VALUES.start_test = start_test
        VALUES.last_request = 0
        VALUES.last_hardware_update = None

        self.error_state = ShutDown()
        self.state_publisher = state_publisher

        self.connections = rospy.get_param("hcm_state_machine")

        self.set_state(Startup())

    def publish_state(self, state):
        msg = RobotControlState()
        msg.state = state.hcm_state()
        self.state_publisher.publish(msg)

    def is_penalized(self):
        return self.state.__class__ in (Penalty, PenaltyAnimationIn, PenaltyAnimationOut)

    def is_walking(self):
        return self.state.__class__ in (Walking, WalkingStopping)

    def is_record(self):
        return self.state.__class__ is Record

    def is_shutdown(self):
        return self.state.__class__ is ShutDown

class Startup(AbstractState):
    def entry(self):
        self.start_time_limit = rospy.get_param("hcm/start_time", 10)

    def evaluate(self):
        if not self.animation_started:
            # leave this if we got a hardware response, or after some time
            if VALUES.last_hardware_update is not None or rospy.get_time() - VALUES.start_up_time > self.start_time_limit:
                # check if we directly go into a special state, if not, got to get up
                if VALUES.start_test:
                    pass
                    return
                if VALUES.record:
                    switch_motor_power(True)
                    return Record()
                if VALUES.soft_start:
                    switch_motor_power(False)
                    # to prohibit getting directly out of softoff
                    VALUES.last_client_update = rospy.get_time() - 120
                    return Softoff()
                switch_motor_power(True)
                if VALUES.penalized:
                    return PenaltyAnimationIn()

                # normal states
                if VALUES.standup_flag:
                    self.next_state = GettingUp()
                else:
                    self.next_state = Controllable()
                # play init
                self.start_animation(rospy.get_param("hcm/animations/init"))
            else:
                rospy.loginfo_throttle(1, "Motion is waiting for data from hardware")
        else:
            if self.animation_finished():
                return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_STARTUP

    def shutdown(self):
        return ShutDown()

class Softoff(AbstractState):
    def entry(self):
        switch_motor_power(False)

    def evaluate(self):
        if not self.animation_started:
            if VALUES.record:
                switch_motor_power(True)
                self.next_state = Record()
                # don't directly change state, we wait for animation to finish
                self.start_animation(rospy.get_param("hcm/animations/walkready"))
                return
            if rospy.get_time() - VALUES.last_request < 10:
                # got a new move request
                switch_motor_power(True)
                self.next_state = Controllable()
                self.start_animation(rospy.get_param("hcm/animations/walkready"))
                return
            if VALUES.is_die_time():
                return ShutDown()
            if VALUES.external_animation_requested:
                switch_motor_power(True)
                return Controllable()
            rospy.sleep(0.1)
        else:
            if self.animation_finished():
                return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        # we want to look controlable to the outside
        return STATE_CONTROLABLE

    def shutdown(self):
        return ShutDown()

class Record(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if not VALUES.record:
            return Controllable()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_RECORD

    def shutdown(self):
        return ShutDownAnimation()

class PenaltyAnimationIn(AbstractState):
    def entry(self):
        rospy.logwarn("Penalized, sitting down!")
        self.next_state = Penalty()
        self.start_animation(rospy.get_param("hcm/animations/penalized"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_PENALTY_ANIMATION

    def shutdown(self):
        return ShutDownAnimation()

class PenaltyAnimationOut(AbstractState):
    def entry(self):
        rospy.logwarn("Not penalized anymore, getting up!")
        self.next_state = Controllable()
        self.start_animation(rospy.get_param("hcm/animations/penalized_end"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_PENALTY_ANIMATION

    def shutdown(self):
        return ShutDownAnimation()

class Penalty(AbstractState):
    def entry(self):
        switch_motor_power(False)
        rospy.loginfo("Im now penalized")

    def evaluate(self):
        if VALUES.penalized:
            # still penalized, lets wait a bit
            rospy.sleep(0.05)
            # prohibit soft off
            VALUES.last_client_update = rospy.get_time()
        else:
            return PenaltyAnimationOut()

    def exit(self):
        switch_motor_power(True)

    def hcm_state(self):
        return STATE_PENALTY

    def shutdown(self):
        return ShutDown()

class GettingUp(AbstractState):
    """This state starts the getting up procedure. """

    def entry(self):
        # normally we should got to getting up second after this
        self.next_state = GettingUpSecond()
        # but lets check if we actually have to stand up
        fallen = VALUES.fall_checker.check_fallen(VALUES.smooth_accel)
        if fallen is not None:
            self.start_animation(fallen)
        else:
            self.next_state = Controllable()
            self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        # wait for animation started in entry
        if self.animation_finished():
            # head on
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_GETTING_UP

    def shutdown(self):
        return ShutDownAnimation()

class GettingUpSecond(AbstractState):
    """This state plays a second animation for getting up."""

    def entry(self):
        self.next_state = Controllable()
        self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        if self.animation_finished():
            # head on
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_GETTING_UP

    def shutdown(self):
        return ShutDownAnimation()

class Controllable(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if VALUES.record:
            return Record()
        if VALUES.penalized:
            return PenaltyAnimationIn()
        if VALUES.is_soft_off_time():
            return Softoff()
        if VALUES.is_die_time():
            rospy.logwarn("die time")
            return ShutDownAnimation()

        if VALUES.standup_flag:
            ## Falling detection
            falling_pose = VALUES.fall_checker.check_falling(VALUES.not_so_smooth_gyro)
            if falling_pose is not None:
                return Falling()
            ### Standing up
            direction_animation = VALUES.fall_checker.check_fallen(VALUES.smooth_accel)
            if direction_animation is not None:
                return Fallen()

        if VALUES.external_animation_playing:
            return AnimationRunning()

        if VALUES.walking_active:
            return Walking()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_CONTROLABLE

    def shutdown(self):
        return ShutDownAnimation()

class Falling(AbstractState):
    def entry(self):
        self.wait_time = rospy.get_time()
        # go directly in falling pose
        falling_pose = VALUES.fall_checker.check_falling(VALUES.not_so_smooth_gyro)
        if falling_pose is not None:
            # we're falling, stay in falling
            self.next_state = Falling()
            self.start_animation(falling_pose)
        else:
            # we're not falling anymore
            if VALUES.is_fallen():
                # go directly to fallen
                return Fallen()
            else:
                self.next_state = Controllable()
                self.start_animation(rospy.get_param("hcm/animations/walkready"))

    def evaluate(self):
        # we wait a moment before going to the next state
        if rospy.get_time() - self.wait_time > 3 and self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_FALLING

    def shutdown(self):
        return ShutDown()

class Fallen(AbstractState):
    def entry(self):
        direction_animation = VALUES.is_fallen()
        if direction_animation is None:
            # we don't have to stand up, go directly to controllable
            return Controllable()
        else:
            # do corresponding animation
            self.next_state = GettingUp()
            self.start_animation(direction_animation)

    def evaluate(self):
        if self.animation_finished():
            return self.next_state

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_FALLEN

    def shutdown(self):
        return ShutDown()

class Walking(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        if VALUES.record:
            return Record()
        if VALUES.penalized:
            return PenaltyAnimationIn()
        if VALUES.is_soft_off_time():
            return Softoff()
        if VALUES.is_die_time():
            return ShutDownAnimation()
        if VALUES.standup_flag:
            # Falling detection
            if VALUES.is_falling():
                return Falling()
            # Standing up
            if VALUES.is_fallen():
                return Fallen()

        if VALUES.external_animation_requested:
            return WalkingStopping()

        if not VALUES.walking_active:
            return Controllable()

    def exit(self):
        VALUES.walking_active = False

    def hcm_state(self):
        return STATE_WALKING

    def shutdown(self):
        return ShutDownAnimation()

class WalkingStopping(AbstractState):
    def entry(self):
        pass

    def evaluate(self):
        return Controllable()

    def exit(self):
        VALUES.walking_active = False

    def hcm_state(self):
        return STATE_WALKING

    def shutdown(self):
        return ShutDownAnimation()

class AnimationRunning(AbstractState):
    def entry(self):
        # reset flags
        VALUES.external_animation_playing = False
        VALUES.external_animation_requested = False

    def evaluate(self):
        # if external animation finished
        if not VALUES.external_animation_finished:
            return Controllable()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_ANIMATION_RUNNING

    def shutdown(self):
        return ShutDownAnimation()

class ShutDownAnimation(AbstractState):
    def entry(self):
        rospy.loginfo("Motion will shut off")
        speak("Motion will shut off", VALUES.speak_publisher, priority=Speak.HIGH_PRIORITY)
        self.start_animation(rospy.get_param("hcm/animations/shut-down"))

    def evaluate(self):
        if self.animation_finished():
            return ShutDown()

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_SHUT_DOWN

    def shutdown(self):
        # we are shutting down, don't change
        return None

class ShutDown(AbstractState):
    def entry(self):
        speak("Motion says good bye", VALUES.speak_publisher, priority=Speak.HIGH_PRIORITY)
        # give humans the chance to hold the robot before turning it off
        rospy.sleep(3)
        switch_motor_power(False)
        rospy.loginfo("Motion is now off, good bye")

    def evaluate(self):
        # do nothing, the state machine will see that we're finished
        pass

    def exit(self):
        pass

    def hcm_state(self):
        return STATE_SHUT_DOWN

    def shutdown(self):
        # this is shut down
        return None

def switch_motor_power(state):
    """ Calling service from CM730 to turn motor power on or off. But only if not using simulator"""
    if rospy.get_param("simulation_active", False):
        rospy.loginfo("I'm simulating, not switching motorpower to " + state.__str__())
    else:
        try:
            rospy.wait_for_service("switch_motor_power", timeout=1)
        except rospy.ROSException:
            rospy.logfatal("Can't switch of motorpower, seems like the CM730 is missing.")
            return
        power_switch = rospy.ServiceProxy("switch_motor_power", SwitchMotorPower)
        try:
            response = power_switch(state)
        except rospy.ServiceException as exc:
            print("Service did not process request: " + str(exc))
        # wait for motors
        rospy.sleep(1)
<|endoftext|>


<commit_before># Copyright 2017 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.

from recipe_engine import recipe_api

class RuntimeApi(recipe_api.RecipeApi):
  """This module assists in experimenting with production recipes.

  For example, when migrating builders from Buildbot to pure LUCI stack.
  """

  def __init__(self, properties, **kwargs):
    super(RuntimeApi, self).__init__(**kwargs)
    self._properties = properties

  @property
  def is_luci(self):
    """True if this recipe is currently running on LUCI stack.

    Should be used only during migration from Buildbot to LUCI stack.
    """
    return bool(self._properties.get('is_luci', False))

  @property
  def is_experimental(self):
    """True if this recipe is currently running in experimental mode.

    Typical usage is to modify steps which produce external side-effects so that
    non-production runs of the recipe do not affect production data.

    Examples:
      * Uploading to an alternate google storage file name when in non-prod mode
      * Appending a 'non-production' tag to external RPCs
    """
    return bool(self._properties.get('is_experimental', False))
<commit_msg>Change is_luci default to True.<commit_after># Copyright 2017 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.

from recipe_engine import recipe_api

class RuntimeApi(recipe_api.RecipeApi):
  """This module assists in experimenting with production recipes.

  For example, when migrating builders from Buildbot to pure LUCI stack.
  """

  def __init__(self, properties, **kwargs):
    super(RuntimeApi, self).__init__(**kwargs)
    self._properties = properties

  @property
  def is_luci(self):
    """True if this recipe is currently running on LUCI stack.

    Should be used only during migration from Buildbot to LUCI stack.
    """
    return bool(self._properties.get('is_luci', True))

  @property
  def is_experimental(self):
    """True if this recipe is currently running in experimental mode.

    Typical usage is to modify steps which produce external side-effects so that
    non-production runs of the recipe do not affect production data.

    Examples:
      * Uploading to an alternate google storage file name when in non-prod mode
      * Appending a 'non-production' tag to external RPCs
    """
    return bool(self._properties.get('is_experimental', False))
<|endoftext|>


<commit_before>from django.test import TestCase
from django.test import Client
from django.core.urlresolvers import reverse

from bluebottle.test.factory_models.payments import OrderPaymentFactory
from bluebottle.test.factory_models.orders import OrderFactory
from bluebottle.test.factory_models.accounts import BlueBottleUserFactory, BlueBottleAddressFactory
from bluebottle.test.utils import FsmTestMixin

from bluebottle.payments.services import PaymentService
from bluebottle.payments_docdata.gateway import DocdataClient
from bluebottle.payments_docdata.adapters import DocdataPaymentAdapter

from bluebottle.utils.utils import StatusDefinition
from bluebottle.payments_docdata.tests.factory_models import DocdataPaymentFactory, DocdataTransactionFactory
from bluebottle.payments.models import OrderPayment
from bluebottle.payments_logger.models import PaymentLogEntry

from mock import patch

# Mock create_payment so we don't need to call the external docdata service
def fake_create_payment(self):
    payment = self.MODEL_CLASS(order_payment=self.order_payment, **self.order_payment.integration_data)
    payment.total_gross_amount = self.order_payment.amount
    payment.payment_cluster_key = 'abc123'
    payment.payment_cluster_id = 'abc123'
    payment.save()

    return payment

class PaymentsDocdataTestCase(TestCase, FsmTestMixin):

    @patch.object(DocdataClient, 'create')
    def setUp(self, mock_client_create):
        # Mock response to creating the payment at docdata
        mock_client_create.return_value = {'order_key': 123, 'order_id': 123}

        # Mock create payment
        mock_create_payment = patch.object(DocdataPaymentAdapter, 'create_payment', fake_create_payment)

        self.order = OrderFactory.create()
        self.order_payment = OrderPaymentFactory.create(order=self.order, payment_method='docdataIdeal',
                                                        integration_data={'default_pm': 'ideal'})
        self.service = PaymentService(order_payment=self.order_payment)

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_check_authorized_status(self, mock_fetch_status, mock_transaction):
        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        self.service.check_payment_status()

        # Check that the status propagated through to order
        self.assert_status(self.order_payment.payment, StatusDefinition.AUTHORIZED)
        self.assert_status(self.order_payment, StatusDefinition.AUTHORIZED)
        self.assert_status(self.order, StatusDefinition.PENDING)

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_no_payment_method_change(self, mock_fetch_status, mock_transaction):
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataCreditcard')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='mastercard',
                                                       payment_cluster_id='1234',
                                                       total_gross_amount=100)
        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='VISA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))

        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataCreditcard')

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_payment_method_change(self, mock_fetch_status, mock_transaction):
        # Two payment log entries already exist: 2x 'a new payment status "started" '
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        # Ensure that we use an existing payment_method or the adapter throws an exception
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataPaypal')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='paypal',
                                                       payment_cluster_id='1235',
                                                       total_gross_amount=100)

        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='VISA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataCreditcard')

        # Check that all is logged correctly
        self.assertEquals(PaymentLogEntry.objects.filter(payment=docdata_payment).count(), 6) # The status changes triggers the
                                                                                # creation of more payment log entries
        log = PaymentLogEntry.objects.all()[0]
        self.assertEqual(log.message,
            "{0} - Payment method changed for payment with id {1} and order payment with id {2}.".format(docdata_payment, docdata_payment.id,
                                                                                docdata_payment.order_payment.id))
        self.assertEqual(log.payment.id, docdata_payment.id)
        self.assertEqual(log.level, 'INFO')

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_unknown_payment_method_change(self, mock_fetch_status, mock_transaction):
        # Two payment log entries already exist: 2x 'a new payment status "started" '
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        # Ensure that we use an existing payment_method or the adapter throws an exception
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataPaypal')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='paypal',
                                                       payment_cluster_id='1236',
                                                       total_gross_amount=100)

        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='BLABLABLA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataPaypal')

        # Check that all is logged correctly
        self.assertEquals(PaymentLogEntry.objects.filter(payment=docdata_payment).count(), 5)
        log = PaymentLogEntry.objects.all()[0]
        self.assertEqual(log.message,
            "{0} - Payment method '{1}' not found for payment with id {2} and order payment with id {3}.".format(
                docdata_payment,
                'BLABLABLA',
                docdata_payment.id,
                docdata_payment.order_payment.id))
        self.assertEqual(log.payment.id, docdata_payment.id)
        self.assertEqual(log.level, 'WARNING')

class AdapterTestCase(TestCase):
    def setUp(self):
        pass

    @patch.object(DocdataClient, 'create')
    def test_incomplete_userdata(self, mock_client_create):
        mock_client_create.return_value = {'order_key': 123, 'order_id': 123}
        mock_create_payment = patch.object(DocdataPaymentAdapter, 'create_payment', fake_create_payment)

        user = BlueBottleUserFactory()
        address = BlueBottleAddressFactory(user=user)
        self.order = OrderFactory.create(user=user)
        self.order_payment = OrderPaymentFactory.create(order=self.order, payment_method='docdataIdeal',
                                                        integration_data={'default_pm': 'ideal'})

        self.service = PaymentService(order_payment=self.order_payment)

        user_data = self.service.adapter.get_user_data()
        self.assertEqual(user_data['id'], user.id)
        self.assertEqual(user_data['first_name'], user.first_name)
        self.assertEqual(user_data['last_name'], user.last_name)
        self.assertEqual(user_data['email'], user.email)
        #self.assertEqual(user_data['ip_address'], None) #Ip is set to None during testing

        self.assertEqual(user_data['street'], 'Unknown')
        self.assertEqual(user_data['house_number'], 'Unknown')
        self.assertEqual(user_data['postal_code'], 'Unknown')
        self.assertEqual(user_data['city'], 'Unknown')
        self.assertEqual(user_data['country'], 'NL')

        self.assertEqual(user_data['company'], '')
        self.assertEqual(user_data['kvk_number'], '')
        self.assertEqual(user_data['vat_number'], '')
        self.assertEqual(user_data['house_number_addition'], '')
        self.assertEqual(user_data['state'], '')

<commit_msg>Fix get user data test<commit_after>from bluebottle.test.factory_models.geo import CountryFactory
from django.test import TestCase
from django.test import Client
from django.core.urlresolvers import reverse

from bluebottle.test.factory_models.payments import OrderPaymentFactory
from bluebottle.test.factory_models.orders import OrderFactory
from bluebottle.test.factory_models.accounts import BlueBottleUserFactory, BlueBottleAddressFactory
from bluebottle.test.utils import FsmTestMixin

from bluebottle.payments.services import PaymentService
from bluebottle.payments_docdata.gateway import DocdataClient
from bluebottle.payments_docdata.adapters import DocdataPaymentAdapter

from bluebottle.utils.utils import StatusDefinition
from bluebottle.payments_docdata.tests.factory_models import DocdataPaymentFactory, DocdataTransactionFactory
from bluebottle.payments.models import OrderPayment
from bluebottle.payments_logger.models import PaymentLogEntry

from mock import patch

# Mock create_payment so we don't need to call the external docdata service
def fake_create_payment(self):
    payment = self.MODEL_CLASS(order_payment=self.order_payment, **self.order_payment.integration_data)
    payment.total_gross_amount = self.order_payment.amount
    payment.payment_cluster_key = 'abc123'
    payment.payment_cluster_id = 'abc123'
    payment.save()

    return payment

class PaymentsDocdataTestCase(TestCase, FsmTestMixin):

    @patch.object(DocdataClient, 'create')
    def setUp(self, mock_client_create):
        # Mock response to creating the payment at docdata
        mock_client_create.return_value = {'order_key': 123, 'order_id': 123}

        # Mock create payment
        mock_create_payment = patch.object(DocdataPaymentAdapter, 'create_payment', fake_create_payment)

        self.order = OrderFactory.create()
        self.order_payment = OrderPaymentFactory.create(order=self.order, payment_method='docdataIdeal',
                                                        integration_data={'default_pm': 'ideal'})
        self.service = PaymentService(order_payment=self.order_payment)

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_check_authorized_status(self, mock_fetch_status, mock_transaction):
        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        self.service.check_payment_status()

        # Check that the status propagated through to order
        self.assert_status(self.order_payment.payment, StatusDefinition.AUTHORIZED)
        self.assert_status(self.order_payment, StatusDefinition.AUTHORIZED)
        self.assert_status(self.order, StatusDefinition.PENDING)

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_no_payment_method_change(self, mock_fetch_status, mock_transaction):
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataCreditcard')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='mastercard',
                                                       payment_cluster_id='1234',
                                                       total_gross_amount=100)
        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='VISA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))

        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataCreditcard')

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_payment_method_change(self, mock_fetch_status, mock_transaction):
        # Two payment log entries already exist: 2x 'a new payment status "started" '
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        # Ensure that we use an existing payment_method or the adapter throws an exception
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataPaypal')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='paypal',
                                                       payment_cluster_id='1235',
                                                       total_gross_amount=100)

        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='VISA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataCreditcard')

        # Check that all is logged correctly
        self.assertEquals(PaymentLogEntry.objects.filter(payment=docdata_payment).count(), 6) # The status changes triggers the
                                                                                # creation of more payment log entries
        log = PaymentLogEntry.objects.all()[0]
        self.assertEqual(log.message,
            "{0} - Payment method changed for payment with id {1} and order payment with id {2}.".format(docdata_payment, docdata_payment.id,
                                                                                docdata_payment.order_payment.id))
        self.assertEqual(log.payment.id, docdata_payment.id)
        self.assertEqual(log.level, 'INFO')

    @patch.object(DocdataPaymentAdapter, '_store_payment_transaction')
    @patch.object(DocdataPaymentAdapter, '_fetch_status')
    def test_unknown_payment_method_change(self, mock_fetch_status, mock_transaction):
        # Two payment log entries already exist: 2x 'a new payment status "started" '
        self.assertEquals(PaymentLogEntry.objects.count(), 2)

        # Mock the status check with docdata
        mock_fetch_status.return_value = self.create_status_response('AUTHORIZED')

        order = OrderFactory.create()
        # Ensure that we use an existing payment_method or the adapter throws an exception
        order_payment = OrderPaymentFactory.create(order=order, payment_method='docdataPaypal')
        docdata_payment = DocdataPaymentFactory.create(order_payment=order_payment,
                                                       default_pm='paypal',
                                                       payment_cluster_id='1236',
                                                       total_gross_amount=100)

        docdata_transaction = DocdataTransactionFactory.create(payment=docdata_payment, payment_method='BLABLABLA')
        c = Client()
        resp = c.get(reverse('docdata-payment-status-update', kwargs={'payment_cluster_id': docdata_payment.payment_cluster_id}))
        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp.content, 'success')

        # Reload the order payment
        order_payment = OrderPayment.objects.get(id=order_payment.id)
        self.assertEqual(order_payment.payment_method, 'docdataPaypal')

        # Check that all is logged correctly
        self.assertEquals(PaymentLogEntry.objects.filter(payment=docdata_payment).count(), 5)
        log = PaymentLogEntry.objects.all()[0]
        self.assertEqual(log.message,
            "{0} - Payment method '{1}' not found for payment with id {2} and order payment with id {3}.".format(
                docdata_payment,
                'BLABLABLA',
                docdata_payment.id,
                docdata_payment.order_payment.id))
        self.assertEqual(log.payment.id, docdata_payment.id)
        self.assertEqual(log.level, 'WARNING')

class AdapterGetUserDataTestCase(TestCase):
    def setUp(self):
        pass

    @patch.object(DocdataClient, 'create')
    def test_incomplete_userdata(self, mock_client_create):
        mock_client_create.return_value = {'order_key': 123, 'order_id': 123}
        mock_create_payment = patch.object(DocdataPaymentAdapter, 'create_payment', fake_create_payment)

        user = BlueBottleUserFactory()
        address = BlueBottleAddressFactory(user=user)
        self.order = OrderFactory.create(user=user)
        self.order_payment = OrderPaymentFactory.create(order=self.order, payment_method='docdataIdeal',
                                                        integration_data={'default_pm': 'ideal'})

        self.service = PaymentService(order_payment=self.order_payment)

        user_data = self.service.adapter.get_user_data()
        self.assertEqual(user_data['id'], user.id)
        self.assertEqual(user_data['first_name'], user.first_name)
        self.assertEqual(user_data['last_name'], user.last_name)
        self.assertEqual(user_data['email'], user.email)

        self.assertEqual(user_data['street'], 'Unknown')
        self.assertEqual(user_data['house_number'], 'Unknown')
        self.assertEqual(user_data['postal_code'], 'Unknown')
        self.assertEqual(user_data['city'], 'Unknown')
        self.assertEqual(user_data['country'], 'NL')

        self.assertEqual(user_data['company'], '')
        self.assertEqual(user_data['kvk_number'], '')
        self.assertEqual(user_data['vat_number'], '')
        self.assertEqual(user_data['house_number_addition'], '')
        self.assertEqual(user_data['state'], '')

    @patch.object(DocdataClient, 'create')
    def test_normal_userdata(self, mock_client_create):
        mock_client_create.return_value = {'order_key': 123, 'order_id': 123}
        mock_create_payment = patch.object(DocdataPaymentAdapter, 'create_payment', fake_create_payment)

        user = BlueBottleUserFactory()
        holland = CountryFactory(name='Netherlands', alpha2_code='NL')

        address = BlueBottleAddressFactory(user=user, line_1='Dam 1a', line_2='', city='Amsterdam',
                                           postal_code='1000AA', country=holland)

        self.order = OrderFactory.create(user=user)
        self.order_payment = OrderPaymentFactory.create(order=self.order, payment_method='docdataIdeal',
                                                        integration_data={'default_pm': 'ideal'})

        self.service = PaymentService(order_payment=self.order_payment)

        user_data = self.service.adapter.get_user_data()
        self.assertEqual(user_data['id'], user.id)
        self.assertEqual(user_data['first_name'], user.first_name)
        self.assertEqual(user_data['last_name'], user.last_name)
        self.assertEqual(user_data['email'], user.email)

        self.assertEqual(user_data['street'], 'Dam')
        self.assertEqual(user_data['house_number'], '1a')
        self.assertEqual(user_data['postal_code'], '1000AA')
        self.assertEqual(user_data['city'], 'Amsterdam')
        self.assertEqual(user_data['country'], 'NL')

        self.assertEqual(user_data['company'], '')
        self.assertEqual(user_data['kvk_number'], '')
        self.assertEqual(user_data['vat_number'], '')
        self.assertEqual(user_data['house_number_addition'], '')
        self.assertEqual(user_data['state'], '')

<|endoftext|>


<commit_before># IF things in this file continue get messy (I'd say 300+ lines) it may be time to
# find a package that parses https://schema.org/Recipe properly (or create one ourselves).

import extruct

from ._exceptions import SchemaOrgException
from ._utils import get_minutes, get_yields, normalize_string

SCHEMA_ORG_HOST = "schema.org"
SCHEMA_NAMES = ["Recipe", "WebPage"]

SYNTAXES = ["json-ld", "microdata"]

class SchemaOrg:
    def __init__(self, page_data, raw=False):
        if raw:
            self.format = "raw"
            self.data = page_data
            return
        self.format = None
        self.data = {}

        data = extruct.extract(page_data, syntaxes=SYNTAXES, errors="log", uniform=True)

        low_schema = {s.lower() for s in SCHEMA_NAMES}
        for syntax in SYNTAXES:
            for item in data.get(syntax, []):
                in_context = SCHEMA_ORG_HOST in item.get("@context", "")
                item_type = item.get("@type", "")
                if isinstance(item_type, list):
                    for type in item_type:
                        if type.lower() in low_schema:
                            item_type = type.lower()
                if in_context and item_type.lower() in low_schema:
                    self.format = syntax
                    self.data = item
                    if item_type.lower() == "webpage":
                        self.data = self.data.get("mainEntity")
                    return
                elif in_context and "@graph" in item:
                    for graph_item in item.get("@graph", ""):
                        graph_item_type = graph_item.get("@type", "")
                        if not isinstance(graph_item_type, str):
                            continue
                        if graph_item_type.lower() in low_schema:
                            in_graph = SCHEMA_ORG_HOST in graph_item.get("@context", "")
                            self.format = syntax
                            if graph_item_type.lower() == "webpage" and in_graph:
                                self.data = self.data.get("mainEntity")
                                return
                            elif graph_item_type.lower() == "recipe":
                                self.data = graph_item
                                return

    def language(self):
        return self.data.get("inLanguage") or self.data.get("language")

    def title(self):
        return normalize_string(self.data.get("name"))

    def category(self):
        category = self.data.get("recipeCategory")
        if isinstance(category, list):
            return ",".join(category)
        return category

    def author(self):
        author = self.data.get("author")
        if (
            author
            and isinstance(author, list)
            and len(author) >= 1
            and isinstance(author[0], dict)
        ):
            author = author[0]
        if author and isinstance(author, dict):
            author = author.get("name")
        return author

    def total_time(self):
        if not (self.data.keys() & {"totalTime", "prepTime", "cookTime"}):
            raise SchemaOrgException("Cooking time information not found in SchemaOrg")

        def get_key_and_minutes(k):
            source = self.data.get(k)
            # Workaround: strictly speaking schema.org does not provide for minValue (and maxValue) properties on objects of type Duration; they are however present on objects with type QuantitativeValue
            # Refs:
            #  - https://schema.org/Duration
            #  - https://schema.org/QuantitativeValue
            if type(source) == dict and "minValue" in source:
                source = source["minValue"]
            return get_minutes(source, return_zero_on_not_found=True)

        total_time = get_key_and_minutes("totalTime")
        if not total_time:
            times = list(map(get_key_and_minutes, ["prepTime", "cookTime"]))
            total_time = sum(times)
        return total_time

    def cook_time(self):
        if not (self.data.keys() & {"cookTime"}):
            raise SchemaOrgException("Cooktime information not found in SchemaOrg")
        return get_minutes(self.data.get("cookTime"), return_zero_on_not_found=True)

    def prep_time(self):
        if not (self.data.keys() & {"prepTime"}):
            raise SchemaOrgException("Preptime information not found in SchemaOrg")
        return get_minutes(self.data.get("prepTime"), return_zero_on_not_found=True)

    def yields(self):
        yield_data = self.data.get("recipeYield")
        if yield_data and isinstance(yield_data, list):
            yield_data = yield_data[0]
        recipe_yield = str(yield_data)
        return get_yields(recipe_yield)

    def image(self):
        image = self.data.get("image")

        if image is None:
            raise SchemaOrgException("Image not found in SchemaOrg")

        if isinstance(image, list):
            # Could contain a dict
            image = image[0]

        if isinstance(image, dict):
            image = image.get("url")

        if "http://" not in image and "https://" not in image:
            # some sites give image path relative to the domain
            # in cases like this handle image url with class methods or og link
            image = ""

        return image

    def ingredients(self):
        ingredients = (
            self.data.get("recipeIngredient") or self.data.get("ingredients") or []
        )
        return [
            normalize_string(ingredient) for ingredient in ingredients if ingredient
        ]

    def nutrients(self):
        nutrients = self.data.get("nutrition", {})

        # Some recipes contain null or numbers which breaks normalize_string()
        # We'll ignore null and convert numbers to a string, like Schema validator does
        for key, val in nutrients.copy().items():
            if val is None:
                del nutrients[key]
            elif type(val) in [int, float]:
                nutrients[key] = str(val)

        return {
            normalize_string(nutrient): normalize_string(value)
            for nutrient, value in nutrients.items()
            if nutrient != "@type" and value is not None
        }

    def _extract_howto_instructions_text(self, schema_item):
        instructions_gist = []
        if type(schema_item) is str:
            instructions_gist.append(schema_item)
        elif schema_item.get("@type") == "HowToStep":
            if schema_item.get("name", False):
                # some sites have duplicated name and text properties (1:1)
                # others have name same as text but truncated to X chars.
                # ignore name in these cases and add the name value only if it's different from the text
                if not schema_item.get("text").startswith(
                    schema_item.get("name").rstrip(".")
                ):
                    instructions_gist.append(schema_item.get("name"))
            instructions_gist.append(schema_item.get("text"))
        elif schema_item.get("@type") == "HowToSection":
            instructions_gist.append(schema_item.get("name") or schema_item.get("Name"))
            for item in schema_item.get("itemListElement"):
                instructions_gist += self._extract_howto_instructions_text(item)
        return instructions_gist

    def instructions(self):
        instructions = self.data.get("recipeInstructions") or ""

        if isinstance(instructions, list):
            instructions_gist = []
            for schema_instruction_item in instructions:
                instructions_gist += self._extract_howto_instructions_text(
                    schema_instruction_item
                )

            return "\n".join(
                normalize_string(instruction) for instruction in instructions_gist
            )

        return instructions

    def ratings(self):
        ratings = self.data.get("aggregateRating")
        if ratings is None:
            raise SchemaOrgException("No ratings data in SchemaOrg.")

        if isinstance(ratings, dict):
            ratings = ratings.get("ratingValue")

        if ratings is None:
            raise SchemaOrgException("No ratingValue in SchemaOrg.")

        return round(float(ratings), 2)

    def cuisine(self):
        cuisine = self.data.get("recipeCuisine")
        if cuisine is None:
            raise SchemaOrgException("No cuisine data in SchemaOrg.")
        elif isinstance(cuisine, list):
            return ",".join(cuisine)
        return cuisine

    def description(self):
        description = self.data.get("description")
        if description is None:
            raise SchemaOrgException("No description data in SchemaOrg.")
        return normalize_string(description)
<commit_msg>Do not log errors from extruct unless package own log level is debug<commit_after># IF things in this file continue get messy (I'd say 300+ lines) it may be time to
# find a package that parses https://schema.org/Recipe properly (or create one ourselves).

import extruct

from recipe_scrapers.settings import settings

from ._exceptions import SchemaOrgException
from ._utils import get_minutes, get_yields, normalize_string

SCHEMA_ORG_HOST = "schema.org"
SCHEMA_NAMES = ["Recipe", "WebPage"]

SYNTAXES = ["json-ld", "microdata"]

class SchemaOrg:
    def __init__(self, page_data, raw=False):
        if raw:
            self.format = "raw"
            self.data = page_data
            return
        self.format = None
        self.data = {}

        data = extruct.extract(
            page_data,
            syntaxes=SYNTAXES,
            errors="log" if settings.LOG_LEVEL <= 10 else "ignore",
            uniform=True,
        )

        low_schema = {s.lower() for s in SCHEMA_NAMES}
        for syntax in SYNTAXES:
            for item in data.get(syntax, []):
                in_context = SCHEMA_ORG_HOST in item.get("@context", "")
                item_type = item.get("@type", "")
                if isinstance(item_type, list):
                    for type in item_type:
                        if type.lower() in low_schema:
                            item_type = type.lower()
                if in_context and item_type.lower() in low_schema:
                    self.format = syntax
                    self.data = item
                    if item_type.lower() == "webpage":
                        self.data = self.data.get("mainEntity")
                    return
                elif in_context and "@graph" in item:
                    for graph_item in item.get("@graph", ""):
                        graph_item_type = graph_item.get("@type", "")
                        if not isinstance(graph_item_type, str):
                            continue
                        if graph_item_type.lower() in low_schema:
                            in_graph = SCHEMA_ORG_HOST in graph_item.get("@context", "")
                            self.format = syntax
                            if graph_item_type.lower() == "webpage" and in_graph:
                                self.data = self.data.get("mainEntity")
                                return
                            elif graph_item_type.lower() == "recipe":
                                self.data = graph_item
                                return

    def language(self):
        return self.data.get("inLanguage") or self.data.get("language")

    def title(self):
        return normalize_string(self.data.get("name"))

    def category(self):
        category = self.data.get("recipeCategory")
        if isinstance(category, list):
            return ",".join(category)
        return category

    def author(self):
        author = self.data.get("author")
        if (
            author
            and isinstance(author, list)
            and len(author) >= 1
            and isinstance(author[0], dict)
        ):
            author = author[0]
        if author and isinstance(author, dict):
            author = author.get("name")
        return author

    def total_time(self):
        if not (self.data.keys() & {"totalTime", "prepTime", "cookTime"}):
            raise SchemaOrgException("Cooking time information not found in SchemaOrg")

        def get_key_and_minutes(k):
            source = self.data.get(k)
            # Workaround: strictly speaking schema.org does not provide for minValue (and maxValue) properties on objects of type Duration; they are however present on objects with type QuantitativeValue
            # Refs:
            #  - https://schema.org/Duration
            #  - https://schema.org/QuantitativeValue
            if type(source) == dict and "minValue" in source:
                source = source["minValue"]
            return get_minutes(source, return_zero_on_not_found=True)

        total_time = get_key_and_minutes("totalTime")
        if not total_time:
            times = list(map(get_key_and_minutes, ["prepTime", "cookTime"]))
            total_time = sum(times)
        return total_time

    def cook_time(self):
        if not (self.data.keys() & {"cookTime"}):
            raise SchemaOrgException("Cooktime information not found in SchemaOrg")
        return get_minutes(self.data.get("cookTime"), return_zero_on_not_found=True)

    def prep_time(self):
        if not (self.data.keys() & {"prepTime"}):
            raise SchemaOrgException("Preptime information not found in SchemaOrg")
        return get_minutes(self.data.get("prepTime"), return_zero_on_not_found=True)

    def yields(self):
        yield_data = self.data.get("recipeYield")
        if yield_data and isinstance(yield_data, list):
            yield_data = yield_data[0]
        recipe_yield = str(yield_data)
        return get_yields(recipe_yield)

    def image(self):
        image = self.data.get("image")

        if image is None:
            raise SchemaOrgException("Image not found in SchemaOrg")

        if isinstance(image, list):
            # Could contain a dict
            image = image[0]

        if isinstance(image, dict):
            image = image.get("url")

        if "http://" not in image and "https://" not in image:
            # some sites give image path relative to the domain
            # in cases like this handle image url with class methods or og link
            image = ""

        return image

    def ingredients(self):
        ingredients = (
            self.data.get("recipeIngredient") or self.data.get("ingredients") or []
        )
        return [
            normalize_string(ingredient) for ingredient in ingredients if ingredient
        ]

    def nutrients(self):
        nutrients = self.data.get("nutrition", {})

        # Some recipes contain null or numbers which breaks normalize_string()
        # We'll ignore null and convert numbers to a string, like Schema validator does
        for key, val in nutrients.copy().items():
            if val is None:
                del nutrients[key]
            elif type(val) in [int, float]:
                nutrients[key] = str(val)

        return {
            normalize_string(nutrient): normalize_string(value)
            for nutrient, value in nutrients.items()
            if nutrient != "@type" and value is not None
        }

    def _extract_howto_instructions_text(self, schema_item):
        instructions_gist = []
        if type(schema_item) is str:
            instructions_gist.append(schema_item)
        elif schema_item.get("@type") == "HowToStep":
            if schema_item.get("name", False):
                # some sites have duplicated name and text properties (1:1)
                # others have name same as text but truncated to X chars.
                # ignore name in these cases and add the name value only if it's different from the text
                if not schema_item.get("text").startswith(
                    schema_item.get("name").rstrip(".")
                ):
                    instructions_gist.append(schema_item.get("name"))
            instructions_gist.append(schema_item.get("text"))
        elif schema_item.get("@type") == "HowToSection":
            instructions_gist.append(schema_item.get("name") or schema_item.get("Name"))
            for item in schema_item.get("itemListElement"):
                instructions_gist += self._extract_howto_instructions_text(item)
        return instructions_gist

    def instructions(self):
        instructions = self.data.get("recipeInstructions") or ""

        if isinstance(instructions, list):
            instructions_gist = []
            for schema_instruction_item in instructions:
                instructions_gist += self._extract_howto_instructions_text(
                    schema_instruction_item
                )

            return "\n".join(
                normalize_string(instruction) for instruction in instructions_gist
            )

        return instructions

    def ratings(self):
        ratings = self.data.get("aggregateRating")
        if ratings is None:
            raise SchemaOrgException("No ratings data in SchemaOrg.")

        if isinstance(ratings, dict):
            ratings = ratings.get("ratingValue")

        if ratings is None:
            raise SchemaOrgException("No ratingValue in SchemaOrg.")

        return round(float(ratings), 2)

    def cuisine(self):
        cuisine = self.data.get("recipeCuisine")
        if cuisine is None:
            raise SchemaOrgException("No cuisine data in SchemaOrg.")
        elif isinstance(cuisine, list):
            return ",".join(cuisine)
        return cuisine

    def description(self):
        description = self.data.get("description")
        if description is None:
            raise SchemaOrgException("No description data in SchemaOrg.")
        return normalize_string(description)
<|endoftext|>