<commit_before># Copyright (c) 2014 AnsibleWorks, Inc.
# All Rights Reserved.

# Python
import datetime
import hashlib
import hmac
import json
import logging
import os
import re
import shlex
import urlparse
import uuid

# Django
from django.conf import settings
from django.db import models
from django.db.models import CASCADE, SET_NULL, PROTECT
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.utils.timezone import now, make_aware, get_default_timezone

# AWX
from awx.lib.compat import slugify
from awx.main.models.base import *
from awx.main.models.jobs import Job
from awx.main.models.unified_jobs import *
from awx.main.utils import update_scm_url
from awx.main.utils import encrypt_field

__all__ = ['Project', 'ProjectUpdate']

class ProjectOptions(models.Model):

    SCM_TYPE_CHOICES = [
        ('', _('Manual')),
        ('git', _('Git')),
        ('hg', _('Mercurial')),
        ('svn', _('Subversion')),
    ]

    class Meta:
        abstract = True

    # Project files must be available on the server in folders directly
    # beneath the path specified by settings.PROJECTS_ROOT.  There is no way
    # via the API to upload/update a project or its playbooks; this must be
    # done by other means for now.

    @classmethod
    def get_local_path_choices(cls):
        if os.path.exists(settings.PROJECTS_ROOT):
            paths = [x for x in os.listdir(settings.PROJECTS_ROOT)
                     if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x))
                     and not x.startswith('.') and not x.startswith('_')]
            qs = Project.objects.filter(active=True)
            used_paths = qs.values_list('local_path', flat=True)
            return [x for x in paths if x not in used_paths]
        else:
            return []

    local_path = models.CharField(
        max_length=1024,
        blank=True,
        help_text=_('Local path (relative to PROJECTS_ROOT) containing '
                    'playbooks and related files for this project.')
    )

    scm_type = models.CharField(
        max_length=8,
        choices=SCM_TYPE_CHOICES,
        blank=True,
        default='',
        verbose_name=_('SCM Type'),
    )
    scm_url = models.CharField(
        max_length=1024,
        blank=True,
        default='',
        verbose_name=_('SCM URL'),
    )
    scm_branch = models.CharField(
        max_length=256,
        blank=True,
        default='',
        verbose_name=_('SCM Branch'),
        help_text=_('Specific branch, tag or commit to checkout.'),
    )
    scm_clean = models.BooleanField(
        default=False,
    )
    scm_delete_on_update = models.BooleanField(
        default=False,
    )
    credential = models.ForeignKey(
        'Credential',
        related_name='%(class)ss',
        blank=True,
        null=True,
        default=None,
        on_delete=models.SET_NULL,
    )

    def clean_scm_type(self):
        return self.scm_type or ''

    def clean_scm_url(self):
        scm_url = unicode(self.scm_url or '')
        if not self.scm_type:
            return ''
        try:
            scm_url = update_scm_url(self.scm_type, scm_url,
                                     check_special_cases=False)
        except ValueError, e:
            raise ValidationError((e.args or ('Invalid SCM URL',))[0])
        scm_url_parts = urlparse.urlsplit(scm_url)
        if self.scm_type and not any(scm_url_parts):
            raise ValidationError('SCM URL is required')
        return unicode(self.scm_url or '')

    def clean_credential(self):
        if not self.scm_type:
            return None
        cred = self.credential
        if cred:
            if cred.kind != 'scm':
                raise ValidationError('Credential kind must be "scm"')
            try:
                scm_url = update_scm_url(self.scm_type, self.scm_url,
                                         check_special_cases=False)
                scm_url_parts = urlparse.urlsplit(scm_url)
                # Prefer the username/password in the URL, if provided.
                scm_username = scm_url_parts.username or cred.username or ''
                if scm_url_parts.password or cred.password:
                    scm_password = '********'
                else:
                    scm_password = ''
                try:
                    update_scm_url(self.scm_type, self.scm_url, scm_username,
                                   scm_password)
                except ValueError, e:
                    raise ValidationError((e.args or ('Invalid credential',))[0])
            except ValueError:
                pass
        return cred

    def get_project_path(self, check_if_exists=True):
        local_path = os.path.basename(self.local_path)
        if local_path and not local_path.startswith('.'):
            proj_path = os.path.join(settings.PROJECTS_ROOT, local_path)
            if not check_if_exists or os.path.exists(proj_path):
                return proj_path

    @property
    def playbooks(self):
        valid_re = re.compile(r'^\s*?-?\s*?(?:hosts|include):\s*?.*?$')
        results = []
        project_path = self.get_project_path()
        if project_path:
            for dirpath, dirnames, filenames in os.walk(unicode(project_path)):
                for filename in filenames:
                    if os.path.splitext(filename)[-1] not in ['.yml', '.yaml']:
                        continue
                    playbook = os.path.join(dirpath, filename)
                    # Filter files that do not have either hosts or top-level
                    # includes. Use regex to allow files with invalid YAML to
                    # show up.
                    matched = False
                    try:
                        for n, line in enumerate(file(playbook)):
                            if valid_re.match(line):
                                matched = True
                            # Any YAML file can also be encrypted with vault;
                            # allow these to be used as the main playbook.
                            elif n == 0 and line.startswith('$ANSIBLE_VAULT;'):
                                matched = True
                    except IOError:
                        continue
                    if not matched:
                        continue
                    playbook = os.path.relpath(playbook, project_path)
                    # Filter files in a roles subdirectory.
                    if 'roles' in playbook.split(os.sep):
                        continue
                    # Filter files in a tasks subdirectory.
                    if 'tasks' in playbook.split(os.sep):
                        continue
                    results.append(playbook)
        return sorted(results, key=lambda x: unicode(x).lower())

class Project(UnifiedJobTemplate, ProjectOptions):
    '''
    A project represents a playbook git repo that can access a set of inventories
    '''

    class Meta:
        app_label = 'main'
        ordering = ('id',)

    scm_delete_on_next_update = models.BooleanField(
        default=False,
        editable=False,
    )
    scm_update_on_launch = models.BooleanField(
        default=False,
    )
    scm_update_cache_timeout = models.PositiveIntegerField(
        default=0,
    )

    @classmethod
    def _get_unified_job_class(cls):
        return ProjectUpdate

    @classmethod
    def _get_unified_job_field_names(cls):
        return ['name', 'description', 'local_path', 'scm_type', 'scm_url',
                'scm_branch', 'scm_clean', 'scm_delete_on_update',
                'credential', 'schedule']

    def save(self, *args, **kwargs):
        new_instance = not bool(self.pk)
        # If update_fields has been specified, add our field names to it,
        # if it hasn't been specified, then we're just doing a normal save.
        update_fields = kwargs.get('update_fields', [])
        # Check if scm_type or scm_url changes.
        if self.pk:
            project_before = self.__class__.objects.get(pk=self.pk)
            if project_before.scm_type != self.scm_type or project_before.scm_url != self.scm_url:
                self.scm_delete_on_next_update = True
                if 'scm_delete_on_next_update' not in update_fields:
                    update_fields.append('scm_delete_on_next_update')
        # Create auto-generated local path if project uses SCM.
        if self.pk and self.scm_type and not self.local_path.startswith('_'):
            slug_name = slugify(unicode(self.name)).replace(u'-', u'_')
            self.local_path = u'_%d__%s' % (self.pk, slug_name)
            if 'local_path' not in update_fields:
                update_fields.append('local_path')
        # Do the actual save.
        super(Project, self).save(*args, **kwargs)
        if new_instance:
            update_fields=[]
            # Generate local_path for SCM after initial save (so we have a PK).
            if self.scm_type and not self.local_path.startswith('_'):
                update_fields.append('local_path')
            if update_fields:
                self.save(update_fields=update_fields)
        # If we just created a new project with SCM, start the initial update.
        if new_instance and self.scm_type:
            self.update()

    def _get_current_status(self):
        if self.scm_type:
            if self.current_update:
                return 'running'
            elif not self.last_job:
                return 'never updated'
            elif self.last_job_failed:
                return 'failed'
            elif not self.get_project_path():
                return 'missing'
            else:
                return 'successful'
        elif not self.get_project_path():
            return 'missing'
        else:
            return 'ok'

    def _get_last_job_run(self):
        if self.scm_type and self.last_job:
            return self.last_job.finished
        else:
            project_path = self.get_project_path()
            if project_path:
                try:
                    mtime = os.path.getmtime(project_path)
                    dt = datetime.datetime.fromtimestamp(mtime)
                    return make_aware(dt, get_default_timezone())
                except os.error:
                    pass

    def _can_update(self):
        return bool(self.scm_type)

    def _update_unified_job_kwargs(self, **kwargs):
        if self.scm_delete_on_next_update:
            kwargs['scm_delete_on_update'] = True
        return kwargs

    def create_project_update(self, **kwargs):
        return self.create_unified_job(**kwargs)

    @property
    def cache_timeout_blocked(self):
        if not self.last_job_run:
            return False
        if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) > now():
            return True
        return False

    @property
    def needs_update_on_launch(self):
        if self.active and self.scm_type and self.scm_update_on_launch:
            if not self.last_job_run:
                return True
            if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) <= now():
                return True
        return False

    def get_absolute_url(self):
        return reverse('api:project_detail', args=(self.pk,))

class ProjectUpdate(UnifiedJob, ProjectOptions):
    '''
    Internal job for tracking project updates from SCM.
    '''

    class Meta:
        app_label = 'main'

    project = models.ForeignKey(
        'Project',
        related_name='project_updates',
        on_delete=models.CASCADE,
        editable=False,
    )

    @classmethod
    def _get_parent_field_name(cls):
        return 'project'

    @classmethod
    def _get_task_class(cls):
        from awx.main.tasks import RunProjectUpdate
        return RunProjectUpdate

    def is_blocked_by(self, obj):
        if type(obj) == ProjectUpdate:
            if self.project == obj.project:
                return True
        if type(obj) == Job:
            if self.project == obj.project:
                return True
        return False

    def socketio_emit_data(self):
        return dict(project_id=self.project.id)

    @property
    def task_impact(self):
        return 20

    def get_absolute_url(self):
        return reverse('api:project_update_detail', args=(self.pk,))

    def _update_parent_instance(self):
        parent_instance = self._get_parent_instance()
        if parent_instance:
            if self.status in ('pending', 'waiting', 'running'):
                if parent_instance.current_job != self:
                    parent_instance.current_job = self
                    parent_instance.save(update_fields=['current_job'])
            elif self.status in ('successful', 'failed', 'error', 'canceled'):
                if parent_instance.current_job == self:
                    parent_instance.current_job = None
                parent_instance.last_job = self
                parent_instance.last_job_failed = self.failed
                if not self.failed and parent_instance.scm_delete_on_next_update:
                    parent_instance.scm_delete_on_next_update = False
                parent_instance.save(update_fields=['current_job',
                                                    'last_job',
                                                    'last_job_failed',
                                                    'scm_delete_on_next_update'])
<commit_msg>Explicitly typecast self.pk.<commit_after># Copyright (c) 2014 AnsibleWorks, Inc.
# All Rights Reserved.

# Python
import datetime
import hashlib
import hmac
import json
import logging
import os
import re
import shlex
import urlparse
import uuid

# Django
from django.conf import settings
from django.db import models
from django.db.models import CASCADE, SET_NULL, PROTECT
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError, NON_FIELD_ERRORS
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.utils.timezone import now, make_aware, get_default_timezone

# AWX
from awx.lib.compat import slugify
from awx.main.models.base import *
from awx.main.models.jobs import Job
from awx.main.models.unified_jobs import *
from awx.main.utils import update_scm_url
from awx.main.utils import encrypt_field

__all__ = ['Project', 'ProjectUpdate']

class ProjectOptions(models.Model):

    SCM_TYPE_CHOICES = [
        ('', _('Manual')),
        ('git', _('Git')),
        ('hg', _('Mercurial')),
        ('svn', _('Subversion')),
    ]

    class Meta:
        abstract = True

    # Project files must be available on the server in folders directly
    # beneath the path specified by settings.PROJECTS_ROOT.  There is no way
    # via the API to upload/update a project or its playbooks; this must be
    # done by other means for now.

    @classmethod
    def get_local_path_choices(cls):
        if os.path.exists(settings.PROJECTS_ROOT):
            paths = [x for x in os.listdir(settings.PROJECTS_ROOT)
                     if os.path.isdir(os.path.join(settings.PROJECTS_ROOT, x))
                     and not x.startswith('.') and not x.startswith('_')]
            qs = Project.objects.filter(active=True)
            used_paths = qs.values_list('local_path', flat=True)
            return [x for x in paths if x not in used_paths]
        else:
            return []

    local_path = models.CharField(
        max_length=1024,
        blank=True,
        help_text=_('Local path (relative to PROJECTS_ROOT) containing '
                    'playbooks and related files for this project.')
    )

    scm_type = models.CharField(
        max_length=8,
        choices=SCM_TYPE_CHOICES,
        blank=True,
        default='',
        verbose_name=_('SCM Type'),
    )
    scm_url = models.CharField(
        max_length=1024,
        blank=True,
        default='',
        verbose_name=_('SCM URL'),
    )
    scm_branch = models.CharField(
        max_length=256,
        blank=True,
        default='',
        verbose_name=_('SCM Branch'),
        help_text=_('Specific branch, tag or commit to checkout.'),
    )
    scm_clean = models.BooleanField(
        default=False,
    )
    scm_delete_on_update = models.BooleanField(
        default=False,
    )
    credential = models.ForeignKey(
        'Credential',
        related_name='%(class)ss',
        blank=True,
        null=True,
        default=None,
        on_delete=models.SET_NULL,
    )

    def clean_scm_type(self):
        return self.scm_type or ''

    def clean_scm_url(self):
        scm_url = unicode(self.scm_url or '')
        if not self.scm_type:
            return ''
        try:
            scm_url = update_scm_url(self.scm_type, scm_url,
                                     check_special_cases=False)
        except ValueError, e:
            raise ValidationError((e.args or ('Invalid SCM URL',))[0])
        scm_url_parts = urlparse.urlsplit(scm_url)
        if self.scm_type and not any(scm_url_parts):
            raise ValidationError('SCM URL is required')
        return unicode(self.scm_url or '')

    def clean_credential(self):
        if not self.scm_type:
            return None
        cred = self.credential
        if cred:
            if cred.kind != 'scm':
                raise ValidationError('Credential kind must be "scm"')
            try:
                scm_url = update_scm_url(self.scm_type, self.scm_url,
                                         check_special_cases=False)
                scm_url_parts = urlparse.urlsplit(scm_url)
                # Prefer the username/password in the URL, if provided.
                scm_username = scm_url_parts.username or cred.username or ''
                if scm_url_parts.password or cred.password:
                    scm_password = '********'
                else:
                    scm_password = ''
                try:
                    update_scm_url(self.scm_type, self.scm_url, scm_username,
                                   scm_password)
                except ValueError, e:
                    raise ValidationError((e.args or ('Invalid credential',))[0])
            except ValueError:
                pass
        return cred

    def get_project_path(self, check_if_exists=True):
        local_path = os.path.basename(self.local_path)
        if local_path and not local_path.startswith('.'):
            proj_path = os.path.join(settings.PROJECTS_ROOT, local_path)
            if not check_if_exists or os.path.exists(proj_path):
                return proj_path

    @property
    def playbooks(self):
        valid_re = re.compile(r'^\s*?-?\s*?(?:hosts|include):\s*?.*?$')
        results = []
        project_path = self.get_project_path()
        if project_path:
            for dirpath, dirnames, filenames in os.walk(unicode(project_path)):
                for filename in filenames:
                    if os.path.splitext(filename)[-1] not in ['.yml', '.yaml']:
                        continue
                    playbook = os.path.join(dirpath, filename)
                    # Filter files that do not have either hosts or top-level
                    # includes. Use regex to allow files with invalid YAML to
                    # show up.
                    matched = False
                    try:
                        for n, line in enumerate(file(playbook)):
                            if valid_re.match(line):
                                matched = True
                            # Any YAML file can also be encrypted with vault;
                            # allow these to be used as the main playbook.
                            elif n == 0 and line.startswith('$ANSIBLE_VAULT;'):
                                matched = True
                    except IOError:
                        continue
                    if not matched:
                        continue
                    playbook = os.path.relpath(playbook, project_path)
                    # Filter files in a roles subdirectory.
                    if 'roles' in playbook.split(os.sep):
                        continue
                    # Filter files in a tasks subdirectory.
                    if 'tasks' in playbook.split(os.sep):
                        continue
                    results.append(playbook)
        return sorted(results, key=lambda x: unicode(x).lower())

class Project(UnifiedJobTemplate, ProjectOptions):
    '''
    A project represents a playbook git repo that can access a set of inventories
    '''

    class Meta:
        app_label = 'main'
        ordering = ('id',)

    scm_delete_on_next_update = models.BooleanField(
        default=False,
        editable=False,
    )
    scm_update_on_launch = models.BooleanField(
        default=False,
    )
    scm_update_cache_timeout = models.PositiveIntegerField(
        default=0,
    )

    @classmethod
    def _get_unified_job_class(cls):
        return ProjectUpdate

    @classmethod
    def _get_unified_job_field_names(cls):
        return ['name', 'description', 'local_path', 'scm_type', 'scm_url',
                'scm_branch', 'scm_clean', 'scm_delete_on_update',
                'credential', 'schedule']

    def save(self, *args, **kwargs):
        new_instance = not bool(self.pk)
        # If update_fields has been specified, add our field names to it,
        # if it hasn't been specified, then we're just doing a normal save.
        update_fields = kwargs.get('update_fields', [])
        # Check if scm_type or scm_url changes.
        if self.pk:
            project_before = self.__class__.objects.get(pk=self.pk)
            if project_before.scm_type != self.scm_type or project_before.scm_url != self.scm_url:
                self.scm_delete_on_next_update = True
                if 'scm_delete_on_next_update' not in update_fields:
                    update_fields.append('scm_delete_on_next_update')
        # Create auto-generated local path if project uses SCM.
        if self.pk and self.scm_type and not self.local_path.startswith('_'):
            slug_name = slugify(unicode(self.name)).replace(u'-', u'_')
            self.local_path = u'_%d__%s' % (int(self.pk), slug_name)
            if 'local_path' not in update_fields:
                update_fields.append('local_path')
        # Do the actual save.
        super(Project, self).save(*args, **kwargs)
        if new_instance:
            update_fields=[]
            # Generate local_path for SCM after initial save (so we have a PK).
            if self.scm_type and not self.local_path.startswith('_'):
                update_fields.append('local_path')
            if update_fields:
                self.save(update_fields=update_fields)
        # If we just created a new project with SCM, start the initial update.
        if new_instance and self.scm_type:
            self.update()

    def _get_current_status(self):
        if self.scm_type:
            if self.current_update:
                return 'running'
            elif not self.last_job:
                return 'never updated'
            elif self.last_job_failed:
                return 'failed'
            elif not self.get_project_path():
                return 'missing'
            else:
                return 'successful'
        elif not self.get_project_path():
            return 'missing'
        else:
            return 'ok'

    def _get_last_job_run(self):
        if self.scm_type and self.last_job:
            return self.last_job.finished
        else:
            project_path = self.get_project_path()
            if project_path:
                try:
                    mtime = os.path.getmtime(project_path)
                    dt = datetime.datetime.fromtimestamp(mtime)
                    return make_aware(dt, get_default_timezone())
                except os.error:
                    pass

    def _can_update(self):
        return bool(self.scm_type)

    def _update_unified_job_kwargs(self, **kwargs):
        if self.scm_delete_on_next_update:
            kwargs['scm_delete_on_update'] = True
        return kwargs

    def create_project_update(self, **kwargs):
        return self.create_unified_job(**kwargs)

    @property
    def cache_timeout_blocked(self):
        if not self.last_job_run:
            return False
        if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) > now():
            return True
        return False

    @property
    def needs_update_on_launch(self):
        if self.active and self.scm_type and self.scm_update_on_launch:
            if not self.last_job_run:
                return True
            if (self.last_job_run + datetime.timedelta(seconds=self.scm_update_cache_timeout)) <= now():
                return True
        return False

    def get_absolute_url(self):
        return reverse('api:project_detail', args=(self.pk,))

class ProjectUpdate(UnifiedJob, ProjectOptions):
    '''
    Internal job for tracking project updates from SCM.
    '''

    class Meta:
        app_label = 'main'

    project = models.ForeignKey(
        'Project',
        related_name='project_updates',
        on_delete=models.CASCADE,
        editable=False,
    )

    @classmethod
    def _get_parent_field_name(cls):
        return 'project'

    @classmethod
    def _get_task_class(cls):
        from awx.main.tasks import RunProjectUpdate
        return RunProjectUpdate

    def is_blocked_by(self, obj):
        if type(obj) == ProjectUpdate:
            if self.project == obj.project:
                return True
        if type(obj) == Job:
            if self.project == obj.project:
                return True
        return False

    def socketio_emit_data(self):
        return dict(project_id=self.project.id)

    @property
    def task_impact(self):
        return 20

    def get_absolute_url(self):
        return reverse('api:project_update_detail', args=(self.pk,))

    def _update_parent_instance(self):
        parent_instance = self._get_parent_instance()
        if parent_instance:
            if self.status in ('pending', 'waiting', 'running'):
                if parent_instance.current_job != self:
                    parent_instance.current_job = self
                    parent_instance.save(update_fields=['current_job'])
            elif self.status in ('successful', 'failed', 'error', 'canceled'):
                if parent_instance.current_job == self:
                    parent_instance.current_job = None
                parent_instance.last_job = self
                parent_instance.last_job_failed = self.failed
                if not self.failed and parent_instance.scm_delete_on_next_update:
                    parent_instance.scm_delete_on_next_update = False
                parent_instance.save(update_fields=['current_job',
                                                    'last_job',
                                                    'last_job_failed',
                                                    'scm_delete_on_next_update'])
<|endoftext|>


<commit_before>"""
Support the OwnTracks platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/
"""
import json
import logging
import threading
from collections import defaultdict

import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert

DEPENDENCIES = ['mqtt']

REGIONS_ENTERED = defaultdict(list)
MOBILE_BEACONS_ACTIVE = defaultdict(list)

BEACON_DEV_ID = 'beacon'

LOCATION_TOPIC = 'owntracks/+/+'
EVENT_TOPIC = 'owntracks/+/+/event'

_LOGGER = logging.getLogger(__name__)

LOCK = threading.Lock()

CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'

def setup_scanner(hass, config, see):
    """Setup an OwnTracks tracker."""
    max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)

    def owntracks_location_update(topic, payload, qos):
        """MQTT message received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typelocation
        try:
            data = json.loads(payload)
        except ValueError:
            # If invalid JSON
            _LOGGER.error(
                'Unable to parse payload as JSON: %s', payload)
            return

        if (not isinstance(data, dict) or data.get('_type') != 'location') or (
                max_gps_accuracy is not None and
                convert(data.get('acc'), float, 0.0) > max_gps_accuracy):
            return

        dev_id, kwargs = _parse_see_args(topic, data)

        # Block updates if we're in a region
        with LOCK:
            if REGIONS_ENTERED[dev_id]:
                _LOGGER.debug(
                    "location update ignored - inside region %s",
                    REGIONS_ENTERED[-1])
                return

            see(**kwargs)
            see_beacons(dev_id, kwargs)

    def owntracks_event_update(topic, payload, qos):
        # pylint: disable=too-many-branches, too-many-statements
        """MQTT event (geofences) received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typetransition
        try:
            data = json.loads(payload)
        except ValueError:
            # If invalid JSON
            _LOGGER.error(
                'Unable to parse payload as JSON: %s', payload)
            return

        if not isinstance(data, dict) or data.get('_type') != 'transition':
            return

        if data.get('desc') is None:
            _LOGGER.error(
                "Location missing from `enter/exit` message - "
                "please turn `Share` on in OwnTracks app")
            return
        # OwnTracks uses - at the start of a beacon zone
        # to switch on 'hold mode' - ignore this
        location = data['desc'].lstrip("-")
        if location.lower() == 'home':
            location = STATE_HOME

        dev_id, kwargs = _parse_see_args(topic, data)

        if data['event'] == 'enter':
            zone = hass.states.get("zone.{}".format(location))
            with LOCK:
                if zone is None:
                    if data['t'] == 'b':
                        # Not a HA zone, and a beacon so assume mobile
                        beacons = MOBILE_BEACONS_ACTIVE[dev_id]
                        if location not in beacons:
                            beacons.append(location)
                        _LOGGER.info("Added beacon %s", location)
                else:
                    # Normal region
                    regions = REGIONS_ENTERED[dev_id]
                    if location not in regions:
                        regions.append(location)
                    _LOGGER.info("Enter region %s", location)
                    _set_gps_from_zone(kwargs, location, zone)

                see(**kwargs)
                see_beacons(dev_id, kwargs)

        elif data['event'] == 'leave':
            with LOCK:
                regions = REGIONS_ENTERED[dev_id]
                if location in regions:
                    regions.remove(location)
                new_region = regions[-1] if regions else None

                if new_region:
                    # Exit to previous region
                    zone = hass.states.get("zone.{}".format(new_region))
                    _set_gps_from_zone(kwargs, new_region, zone)
                    _LOGGER.info("Exit to %s", new_region)
                    see(**kwargs)
                    see_beacons(dev_id, kwargs)

                else:
                    _LOGGER.info("Exit to GPS")
                    # Check for GPS accuracy
                    if not ('acc' in data and
                            max_gps_accuracy is not None and
                            data['acc'] > max_gps_accuracy):

                        see(**kwargs)
                        see_beacons(dev_id, kwargs)
                    else:
                        _LOGGER.info("Inaccurate GPS reported")

                beacons = MOBILE_BEACONS_ACTIVE[dev_id]
                if location in beacons:
                    beacons.remove(location)
                    _LOGGER.info("Remove beacon %s", location)

        else:
            _LOGGER.error(
                'Misformatted mqtt msgs, _type=transition, event=%s',
                data['event'])
            return

    def see_beacons(dev_id, kwargs_param):
        """Set active beacons to the current location."""
        kwargs = kwargs_param.copy()
        # the battery state applies to the tracking device, not the beacon
        kwargs.pop('battery', None)
        for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
            kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
            kwargs['host_name'] = beacon
            see(**kwargs)

    mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
    mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)

    return True

def _parse_see_args(topic, data):
    """Parse the OwnTracks location parameters, into the format see expects."""
    parts = topic.split('/')
    dev_id = '{}_{}'.format(parts[1], parts[2])
    host_name = parts[1]
    kwargs = {
        'dev_id': dev_id,
        'host_name': host_name,
        'gps': (data['lat'], data['lon'])
    }
    if 'acc' in data:
        kwargs['gps_accuracy'] = data['acc']
    if 'batt' in data:
        kwargs['battery'] = data['batt']
    return dev_id, kwargs

def _set_gps_from_zone(kwargs, location, zone):
    """Set the see parameters from the zone parameters."""
    if zone is not None:
        kwargs['gps'] = (
            zone.attributes['latitude'],
            zone.attributes['longitude'])
        kwargs['gps_accuracy'] = zone.attributes['radius']
        kwargs['location_name'] = location
    return kwargs
<commit_msg>Make owntracks.py pylint clean (#1808)<commit_after>"""
Support the OwnTracks platform.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/
"""
import json
import logging
import threading
from collections import defaultdict

import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME
from homeassistant.util import convert

DEPENDENCIES = ['mqtt']

REGIONS_ENTERED = defaultdict(list)
MOBILE_BEACONS_ACTIVE = defaultdict(list)

BEACON_DEV_ID = 'beacon'

LOCATION_TOPIC = 'owntracks/+/+'
EVENT_TOPIC = 'owntracks/+/+/event'

_LOGGER = logging.getLogger(__name__)

LOCK = threading.Lock()

CONF_MAX_GPS_ACCURACY = 'max_gps_accuracy'

def setup_scanner(hass, config, see):
    """Setup an OwnTracks tracker."""
    max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY)

    def validate_payload(payload, data_type):
        """Validate OwnTracks payload."""
        try:
            data = json.loads(payload)
        except ValueError:
            # If invalid JSON
            _LOGGER.error('Unable to parse payload as JSON: %s', payload)
            return None
        if not isinstance(data, dict) or data.get('_type') != data_type:
            _LOGGER.debug('Skipping %s update for following data '
                          'because of missing or malformatted data: %s',
                          data_type, data)
            return None
        if max_gps_accuracy is not None and \
                convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
            _LOGGER.debug('Skipping %s update because expected GPS '
                          'accuracy %s is not met: %s',
                          data_type, max_gps_accuracy, data)
            return None
        return data

    def owntracks_location_update(topic, payload, qos):
        """MQTT message received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typelocation
        data = validate_payload(payload, 'location')
        if not data:
            return

        dev_id, kwargs = _parse_see_args(topic, data)

        # Block updates if we're in a region
        with LOCK:
            if REGIONS_ENTERED[dev_id]:
                _LOGGER.debug(
                    "location update ignored - inside region %s",
                    REGIONS_ENTERED[-1])
                return

            see(**kwargs)
            see_beacons(dev_id, kwargs)

    def owntracks_event_update(topic, payload, qos):
        """MQTT event (geofences) received."""
        # Docs on available data:
        # http://owntracks.org/booklet/tech/json/#_typetransition
        data = validate_payload(payload, 'transition')
        if not data:
            return

        if data.get('desc') is None:
            _LOGGER.error(
                "Location missing from `Entering/Leaving` message - "
                "please turn `Share` on in OwnTracks app")
            return
        # OwnTracks uses - at the start of a beacon zone
        # to switch on 'hold mode' - ignore this
        location = data['desc'].lstrip("-")
        if location.lower() == 'home':
            location = STATE_HOME

        dev_id, kwargs = _parse_see_args(topic, data)

        def enter_event():
            """Execute enter event."""
            zone = hass.states.get("zone.{}".format(location))
            with LOCK:
                if zone is None and data['t'] == 'b':
                    # Not a HA zone, and a beacon so assume mobile
                    beacons = MOBILE_BEACONS_ACTIVE[dev_id]
                    if location not in beacons:
                        beacons.append(location)
                    _LOGGER.info("Added beacon %s", location)
                else:
                    # Normal region
                    regions = REGIONS_ENTERED[dev_id]
                    if location not in regions:
                        regions.append(location)
                    _LOGGER.info("Enter region %s", location)
                    _set_gps_from_zone(kwargs, location, zone)

                see(**kwargs)
                see_beacons(dev_id, kwargs)

        def leave_event():
            """Execute leave event."""
            with LOCK:
                regions = REGIONS_ENTERED[dev_id]
                if location in regions:
                    regions.remove(location)
                new_region = regions[-1] if regions else None

                if new_region:
                    # Exit to previous region
                    zone = hass.states.get("zone.{}".format(new_region))
                    _set_gps_from_zone(kwargs, new_region, zone)
                    _LOGGER.info("Exit to %s", new_region)
                    see(**kwargs)
                    see_beacons(dev_id, kwargs)

                else:
                    _LOGGER.info("Exit to GPS")
                    # Check for GPS accuracy
                    if not ('acc' in data and
                            max_gps_accuracy is not None and
                            data['acc'] > max_gps_accuracy):

                        see(**kwargs)
                        see_beacons(dev_id, kwargs)
                    else:
                        _LOGGER.info("Inaccurate GPS reported")

                beacons = MOBILE_BEACONS_ACTIVE[dev_id]
                if location in beacons:
                    beacons.remove(location)
                    _LOGGER.info("Remove beacon %s", location)

        if data['event'] == 'enter':
            enter_event()
        elif data['event'] == 'leave':
            leave_event()
        else:
            _LOGGER.error(
                'Misformatted mqtt msgs, _type=transition, event=%s',
                data['event'])
            return

    def see_beacons(dev_id, kwargs_param):
        """Set active beacons to the current location."""
        kwargs = kwargs_param.copy()
        # the battery state applies to the tracking device, not the beacon
        kwargs.pop('battery', None)
        for beacon in MOBILE_BEACONS_ACTIVE[dev_id]:
            kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon)
            kwargs['host_name'] = beacon
            see(**kwargs)

    mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
    mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)

    return True

def _parse_see_args(topic, data):
    """Parse the OwnTracks location parameters, into the format see expects."""
    parts = topic.split('/')
    dev_id = '{}_{}'.format(parts[1], parts[2])
    host_name = parts[1]
    kwargs = {
        'dev_id': dev_id,
        'host_name': host_name,
        'gps': (data['lat'], data['lon'])
    }
    if 'acc' in data:
        kwargs['gps_accuracy'] = data['acc']
    if 'batt' in data:
        kwargs['battery'] = data['batt']
    return dev_id, kwargs

def _set_gps_from_zone(kwargs, location, zone):
    """Set the see parameters from the zone parameters."""
    if zone is not None:
        kwargs['gps'] = (
            zone.attributes['latitude'],
            zone.attributes['longitude'])
        kwargs['gps_accuracy'] = zone.attributes['radius']
        kwargs['location_name'] = location
    return kwargs
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of PyBOSSA.
#
# PyBOSSA is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyBOSSA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with PyBOSSA.  If not, see <http://www.gnu.org/licenses/>.

import json
from optparse import OptionParser
import pbclient

def handle_arguments():
    # Arguments for the application
    usage = "usage: %prog [options]"
    parser = OptionParser(usage)
    # URL where PyBossa listens
    parser.add_option("-s", "--server", dest="api_url",
                      help="PyBossa URL http://domain.com/", metavar="URL",
                      default="http://localhost:5000/")
    # API-KEY
    parser.add_option("-k", "--api-key", dest="api_key",
                      help="PyBossa User API-KEY to interact with PyBossa",
                      metavar="API-KEY")
    # Create App
    parser.add_option("-c", "--create-app", action="store_true",
                      dest="create_app",
                      help="Create the application",
                      metavar="CREATE-APP")
    # Update template for tasks and long_description for app
    parser.add_option("-u", "--update-template", action="store_true",
                      dest="update_template",
                      help="Update Tasks template",
                      metavar="UPDATE-TEMPLATE")

    # Update tasks question
    parser.add_option("-q", "--update-tasks",
                      type="int",
                      dest="update_tasks",
                      help="Update Tasks n_answers",
                      metavar="UPDATE-TASKS")

    parser.add_option("-t", "--create-task",
                      dest="create_task",
                      help="Create a task",
                      metavar="CREATE-MORE-TASK")

    # Modify the number of TaskRuns per Task
    # (default 30)
    parser.add_option("-n", "--number-answers",
                      type="int",
                      dest="n_answers",
                      help="Number of answers per task",
                      metavar="N-ANSWERS",
                      default=30)

    parser.add_option("-a", "--application-config",
                      dest="app_config",
                      help="Application config file",
                      metavar="APP-CONFIG",
                      default="app.json")

    parser.add_option("-v", "--verbose", action="store_true", dest="verbose")
    (options, args) = parser.parse_args()

    if not options.create_app and not options.update_template\
            and not options.create_task and not options.update_tasks:
        parser.error("Please check --help or -h for the available options")

    if not options.api_key:
        parser.error("You must supply an API-KEY to create an \
                      application and tasks in PyBossa")

    return options

def get_configuration():
    options = handle_arguments()

    # Load app details
    try:
        with file(options.app_config) as app_json:
            app_config = json.load(app_json)
    except IOError as e:
        print "application config file is missing! Please create a new one"
        exit(1)

    return (app_config, options)

def contents(filename):
    return file(filename).read()

def run(app_config, options):
    def find_app_by_short_name():
        return pbclient.find_app(short_name=app_config['short_name'])[0]

    def setup_app():
        app = find_app_by_short_name()
        app.long_description = contents('long_description.html')
        app.info['task_presenter'] = contents('template.html')
        app.info['thumbnail'] = app_config['thumbnail']
        app.info['tutorial'] = contents('tutorial.html')

        pbclient.update_app(app)
        return app

    def create_task(app):
        # Data for the tasks
        task_info = json.loads(options.create_task)
        task_info["question"] = app_config['question']
        task_info["n_answers"] = options.n_answers
        pbclient.create_task(app.id, task_info)

    pbclient.set('api_key', options.api_key)
    pbclient.set('endpoint', options.api_url)

    if options.verbose:
        print('Running against PyBosssa instance at: %s' % options.api_url)
        print('Using API-KEY: %s' % options.api_key)

    if options.create_app:
        pbclient.create_app(app_config['name'],
                            app_config['short_name'],
                            app_config['description'])

        app = setup_app()
    else:
        app = find_app_by_short_name()

    if options.create_task:
        create_task(app)

    if options.update_template:
        print "Updating app template"
        # discard return value
        setup_app()

    if options.update_tasks:
        def tasks(app):
            offset = 0
            limit = 100
            while True:
                tasks = pbclient.get_tasks(app.id, offset=offset, limit=limit)
                if len(tasks) == 0:
                    break
                for task in tasks:
                    yield task
                offset += len(tasks)

        def update_task(task, count):
            print "Updating task: %s" % task.id
            if 'n_answers' in task.info:
                del(task.info['n_answers'])
            task.n_answers = options.update_tasks
            pbclient.update_task(task)
            count[0] += 1

        print "Updating task n_answers"
        app = find_app_by_short_name()

        n_tasks = [0]
        [update_task(t, n_tasks) for t in tasks(app)]
        print "%s Tasks have been updated!" % n_tasks[0]

if __name__ == "__main__":
    run(*get_configuration())
<commit_msg>Use django templating system<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

# This file is part of PyBOSSA.
#
# PyBOSSA is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyBOSSA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with PyBOSSA.  If not, see <http://www.gnu.org/licenses/>.

import json
from optparse import OptionParser
import pbclient
import os.path

import django.template.loader
import django.conf

django.conf.settings.configure(TEMPLATE_DIRS=(os.path.dirname(__file__),))

def handle_arguments():
    # Arguments for the application
    usage = "usage: %prog [options]"
    parser = OptionParser(usage)
    # URL where PyBossa listens
    parser.add_option("-s", "--server", dest="api_url",
                      help="PyBossa URL http://domain.com/", metavar="URL",
                      default="http://localhost:5000/")
    # API-KEY
    parser.add_option("-k", "--api-key", dest="api_key",
                      help="PyBossa User API-KEY to interact with PyBossa",
                      metavar="API-KEY")
    # Create App
    parser.add_option("-c", "--create-app", action="store_true",
                      dest="create_app",
                      help="Create the application",
                      metavar="CREATE-APP")
    # Update template for tasks and long_description for app
    parser.add_option("-u", "--update-template", action="store_true",
                      dest="update_template",
                      help="Update Tasks template",
                      metavar="UPDATE-TEMPLATE")

    # Update tasks question
    parser.add_option("-q", "--update-tasks",
                      type="int",
                      dest="update_tasks",
                      help="Update Tasks n_answers",
                      metavar="UPDATE-TASKS")

    parser.add_option("-t", "--create-task",
                      dest="create_task",
                      help="Create a task",
                      metavar="CREATE-MORE-TASK")

    # Modify the number of TaskRuns per Task
    # (default 30)
    parser.add_option("-n", "--number-answers",
                      type="int",
                      dest="n_answers",
                      help="Number of answers per task",
                      metavar="N-ANSWERS",
                      default=30)

    parser.add_option("-a", "--application-config",
                      dest="app_config",
                      help="Application config file",
                      metavar="APP-CONFIG",
                      default="app.json")

    parser.add_option("-v", "--verbose", action="store_true", dest="verbose")
    (options, args) = parser.parse_args()

    if not options.create_app and not options.update_template\
            and not options.create_task and not options.update_tasks:
        parser.error("Please check --help or -h for the available options")

    if not options.api_key:
        parser.error("You must supply an API-KEY to create an \
                      application and tasks in PyBossa")

    return options

def get_configuration():
    options = handle_arguments()

    # Load app details
    try:
        with file(options.app_config) as app_json:
            app_config = json.load(app_json)
    except IOError as e:
        print "application config file is missing! Please create a new one"
        exit(1)

    return (app_config, options)

def contents(filename):
    return django.template.loader.render_to_string(filename, {})

def run(app_config, options):
    def find_app_by_short_name():
        return pbclient.find_app(short_name=app_config['short_name'])[0]

    def setup_app():
        app = find_app_by_short_name()
        app.long_description = contents('long_description.html')
        app.info['task_presenter'] = contents('template.html')
        app.info['thumbnail'] = app_config['thumbnail']
        app.info['tutorial'] = contents('tutorial.html')

        pbclient.update_app(app)
        return app

    def create_task(app):
        # Data for the tasks
        task_info = json.loads(options.create_task)
        task_info["question"] = app_config['question']
        task_info["n_answers"] = options.n_answers
        pbclient.create_task(app.id, task_info)

    pbclient.set('api_key', options.api_key)
    pbclient.set('endpoint', options.api_url)

    if options.verbose:
        print('Running against PyBosssa instance at: %s' % options.api_url)
        print('Using API-KEY: %s' % options.api_key)

    if options.create_app:
        pbclient.create_app(app_config['name'],
                            app_config['short_name'],
                            app_config['description'])

        app = setup_app()
    else:
        app = find_app_by_short_name()

    if options.create_task:
        create_task(app)

    if options.update_template:
        print "Updating app template"
        # discard return value
        setup_app()

    if options.update_tasks:
        def tasks(app):
            offset = 0
            limit = 100
            while True:
                tasks = pbclient.get_tasks(app.id, offset=offset, limit=limit)
                if len(tasks) == 0:
                    break
                for task in tasks:
                    yield task
                offset += len(tasks)

        def update_task(task, count):
            print "Updating task: %s" % task.id
            if 'n_answers' in task.info:
                del(task.info['n_answers'])
            task.n_answers = options.update_tasks
            pbclient.update_task(task)
            count[0] += 1

        print "Updating task n_answers"
        app = find_app_by_short_name()

        n_tasks = [0]
        [update_task(t, n_tasks) for t in tasks(app)]
        print "%s Tasks have been updated!" % n_tasks[0]

if __name__ == "__main__":
    run(*get_configuration())
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# Copyright (c) 2015, ESS LLP and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from frappe.utils import getdate
from frappe import _
import datetime
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account,get_income_account
from erpnext.hr.doctype.employee.employee import is_holiday

class PatientAppointment(Document):
	def on_update(self):
		today = datetime.date.today()
		appointment_date = getdate(self.appointment_date)

		# If appointment created for today set as open
		if today == appointment_date:
			frappe.db.set_value("Patient Appointment", self.name, "status", "Open")
			self.reload()

	def after_insert(self):
		# Check fee validity exists
		appointment = self
		validity_exist = validity_exists(appointment.physician, appointment.patient)
		if validity_exist:
			fee_validity = frappe.get_doc("Fee Validity", validity_exist[0][0])

			# Check if the validity is valid
			appointment_date = getdate(appointment.appointment_date)
			if (fee_validity.valid_till >= appointment_date) and (fee_validity.visited < fee_validity.max_visit):
				visited = fee_validity.visited + 1
				frappe.db.set_value("Fee Validity", fee_validity.name, "visited", visited)
				if fee_validity.ref_invoice:
					frappe.db.set_value("Patient Appointment", appointment.name, "sales_invoice", fee_validity.ref_invoice)
				frappe.msgprint(_("{0} has fee validity till {1}").format(appointment.patient, fee_validity.valid_till))
		confirm_sms(self)

	def create_invoice(self):
		return invoice_appointment(self)

def appointment_cancel(appointment_id):
	appointment = frappe.get_doc("Patient Appointment", appointment_id)

	# If invoice --> fee_validity update with -1 visit
	if appointment.sales_invoice:
		validity = frappe.db.exists({"doctype": "Fee Validity", "ref_invoice": appointment.sales_invoice})
		if validity:
			fee_validity = frappe.get_doc("Fee Validity", validity[0][0])
			visited = fee_validity.visited - 1
			frappe.db.set_value("Fee Validity", fee_validity.name, "visited", visited)
			if visited <= 0:
				frappe.msgprint(
					_("Appointment cancelled, Please review and cancel the invoice {0}".format(appointment.sales_invoice))
				)
			else:
				frappe.msgprint(_("Appointment cancelled"))

@frappe.whitelist()
def get_availability_data(date, physician):
	"""
	Get availability data of 'physician' on 'date'
	:param date: Date to check in schedule
	:param physician: Name of the physician
	:return: dict containing a list of available slots, list of appointments and time of appointments
	"""

	date = getdate(date)
	weekday = date.strftime("%A")

	available_slots = []
	slot_details = []
	physician_schedule = None

	employee = None

	physician_obj = frappe.get_doc("Physician", physician)

	# Get Physician employee relation
	if physician_obj.employee:
		employee = physician_obj.employee
	elif physician_obj.user_id:
		if frappe.db.exists({
			"doctype": "Employee",
			"user_id": physician_obj.user_id
			}):
			employee = frappe.get_doc("Employee", {"user_id": physician_obj.user_id}).name

	if employee:
		# Check if it is Holiday
		if is_holiday(employee, date):
			frappe.throw(_("{0} is a company holiday".format(date)))

		# Check if He/She on Leave
		leave_record = frappe.db.sql("""select half_day from `tabLeave Application`
			where employee = %s and %s between from_date and to_date
			and docstatus = 1""", (employee, date), as_dict=True)
		if leave_record:
			if leave_record[0].half_day:
				frappe.throw(_("Dr {0} on Half day Leave on {1}").format(physician, date))
			else:
				frappe.throw(_("Dr {0} on Leave on {1}").format(physician, date))

	# get physicians schedule
	if physician_obj.physician_schedules:
		for schedule in physician_obj.physician_schedules:
			if schedule.schedule:
				physician_schedule = frappe.get_doc("Physician Schedule", schedule.schedule)
			else:
				frappe.throw(_("Dr {0} does not have a Physician Schedule. Add it in Physician master".format(physician)))

			if physician_schedule:
				available_slots = []
				for t in physician_schedule.time_slots:
					if weekday == t.day:
						available_slots.append(t)

				if available_slots:
					appointments = []
				 	if schedule.service_unit:
						slot_name  = schedule.schedule+" - "+schedule.service_unit
						allow_overlap = frappe.get_value('Patient Service Unit', schedule.service_unit, 'overlap_appointments')
						if allow_overlap:
							# fetch all appointments to physician by service unit
							appointments = frappe.get_all(
								"Patient Appointment",
								filters={"physician": physician, "service_unit": schedule.service_unit, "appointment_date": date, "status": ["not in",["Cancelled"]]},
								fields=["name", "appointment_time", "duration", "status"])
						else:
							# fetch all appointments to service unit
							appointments = frappe.get_all(
								"Patient Appointment",
								filters={"service_unit": schedule.service_unit, "appointment_date": date, "status": ["not in",["Cancelled"]]},
								fields=["name", "appointment_time", "duration", "status"])
					else:
						slot_name = schedule.schedule
						# fetch all appointments to physician without service unit
						appointments = frappe.get_all(
							"Patient Appointment",
							filters={"physician": physician, "service_unit": '', "appointment_date": date, "status": ["not in",["Cancelled"]]},
							fields=["name", "appointment_time", "duration", "status"])

					slot_details.append({"slot_name":slot_name, "service_unit":schedule.service_unit,
						"avail_slot":available_slots, 'appointments': appointments})

	else:
		frappe.throw(_("Dr {0} does not have a Physician Schedule. Add it in Physician master".format(physician)))

	if not available_slots and not slot_details:
		# TODO: return available slots in nearby dates
		frappe.throw(_("Physician not available on {0}").format(weekday))

	return {
		"slot_details": slot_details
	}

@frappe.whitelist()
def update_status(appointment_id, status):
	frappe.db.set_value("Patient Appointment", appointment_id, "status", status)
	if status == "Cancelled":
		appointment_cancel(appointment_id)

@frappe.whitelist()
def set_open_appointments():
	today = getdate()
	frappe.db.sql(
		"update `tabPatient Appointment` set status='Open' where status = 'Scheduled'"
		" and appointment_date = %s", today)

@frappe.whitelist()
def set_pending_appointments():
	today = getdate()
	frappe.db.sql(
		"update `tabPatient Appointment` set status='Pending' where status in "
		"('Scheduled','Open') and appointment_date < %s", today)

def confirm_sms(doc):
	if frappe.db.get_value("Healthcare Settings", None, "app_con") == '1':
		message = frappe.db.get_value("Healthcare Settings", None, "app_con_msg")
		send_message(doc, message)

@frappe.whitelist()
def invoice_appointment(appointment_doc):
	if not appointment_doc.name:
		return False
	sales_invoice = frappe.new_doc("Sales Invoice")
	sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer")
	sales_invoice.appointment = appointment_doc.name
	sales_invoice.due_date = getdate()
	sales_invoice.is_pos = '0'
	sales_invoice.debit_to = get_receivable_account(appointment_doc.company)

	fee_validity = get_fee_validity(appointment_doc.physician, appointment_doc.patient, appointment_doc.appointment_date)
	create_invoice_items(appointment_doc.physician, appointment_doc.company, sales_invoice)

	sales_invoice.save(ignore_permissions=True)
	frappe.db.sql("""update `tabPatient Appointment` set sales_invoice=%s where name=%s""", (sales_invoice.name, appointment_doc.name))
	frappe.db.set_value("Fee Validity", fee_validity.name, "ref_invoice", sales_invoice.name)
	consultation = frappe.db.exists({
			"doctype": "Consultation",
			"appointment": appointment_doc.name})
	if consultation:
		frappe.db.set_value("Consultation", consultation[0][0], "invoice", sales_invoice.name)
	return sales_invoice.name

def get_fee_validity(physician, patient, date):
	validity_exist = validity_exists(physician, patient)
	if validity_exist:
		fee_validity = frappe.get_doc("Fee Validity", validity_exist[0][0])
		fee_validity = update_fee_validity(fee_validity, date)
	else:
		fee_validity = create_fee_validity(physician, patient, date)
	return fee_validity

def validity_exists(physician, patient):
	return frappe.db.exists({
			"doctype": "Fee Validity",
			"physician": physician,
			"patient": patient})

def update_fee_validity(fee_validity, date):
	max_visit = frappe.db.get_value("Healthcare Settings", None, "max_visit")
	valid_days = frappe.db.get_value("Healthcare Settings", None, "valid_days")
	if not valid_days:
		valid_days = 1
	if not max_visit:
		max_visit = 1
	date = getdate(date)
	valid_till = date + datetime.timedelta(days=int(valid_days))
	fee_validity.max_visit = max_visit
	fee_validity.visited = 1
	fee_validity.valid_till = valid_till
	fee_validity.save(ignore_permissions=True)
	return fee_validity

def create_fee_validity(physician, patient, date):
	fee_validity = frappe.new_doc("Fee Validity")
	fee_validity.physician = physician
	fee_validity.patient = patient
	fee_validity = update_fee_validity(fee_validity, date)
	return fee_validity

def create_invoice_items(physician, company, invoice):
	item_line = invoice.append("items")
	item_line.item_name = "Consulting Charges"
	item_line.description = "Consulting Charges:  " + physician
	item_line.qty = 1
	item_line.uom = "Nos"
	item_line.conversion_factor = 1
	item_line.income_account = get_income_account(physician, company)
	op_consulting_charge = frappe.db.get_value("Physician", physician, "op_consulting_charge")
	if op_consulting_charge:
		item_line.rate = op_consulting_charge
		item_line.amount = op_consulting_charge
	return invoice

@frappe.whitelist()
def create_consultation(appointment):
	appointment = frappe.get_doc("Patient Appointment", appointment)
	consultation = frappe.new_doc("Consultation")
	consultation.appointment = appointment.name
	consultation.patient = appointment.patient
	consultation.physician = appointment.physician
	consultation.visit_department = appointment.department
	consultation.patient_sex = appointment.patient_sex
	consultation.consultation_date = appointment.appointment_date
	if appointment.sales_invoice:
		consultation.invoice = appointment.sales_invoice
	return consultation.as_dict()

def remind_appointment():
	if frappe.db.get_value("Healthcare Settings", None, "app_rem") == '1':
		rem_before = datetime.datetime.strptime(frappe.get_value("Healthcare Settings", None, "rem_before"), "%H:%M:%S")
		rem_dt = datetime.datetime.now() + datetime.timedelta(
			hours=rem_before.hour, minutes=rem_before.minute, seconds=rem_before.second)

		appointment_list = frappe.db.sql(
			"select name from `tabPatient Appointment` where start_dt between %s and %s and reminded = 0 ",
			(datetime.datetime.now(), rem_dt)
		)

		for i in range(0, len(appointment_list)):
			doc = frappe.get_doc("Patient Appointment", appointment_list[i][0])
			message = frappe.db.get_value("Healthcare Settings", None, "app_rem_msg")
			send_message(doc, message)
			frappe.db.set_value("Patient Appointment", doc.name, "reminded",1)

def send_message(doc, message):
	patient = frappe.get_doc("Patient", doc.patient)
	if patient.mobile:
		context = {"doc": doc, "alert": doc, "comments": None}
		if doc.get("_comments"):
			context["comments"] = json.loads(doc.get("_comments"))

		# jinja to string convertion happens here
		message = frappe.render_template(message, context)
		number = [patient.mobile]
		send_sms(number, message)

@frappe.whitelist()
def get_events(start, end, filters=None):
	"""Returns events for Gantt / Calendar view rendering.

	:param start: Start date-time.
	:param end: End date-time.
	:param filters: Filters (JSON).
	"""
	from frappe.desk.calendar import get_event_conditions
	conditions = get_event_conditions("Patient Appointment", filters)
	data = frappe.db.sql("""select name, patient, physician, status,
		duration, timestamp(appointment_date, appointment_time) as
		'start' from `tabPatient Appointment` where
		(appointment_date between %(start)s and %(end)s)
		and docstatus < 2 {conditions}""".format(conditions=conditions),
		{"start": start, "end": end}, as_dict=True, update={"allDay": 0})
	for item in data:
		item.end = item.start + datetime.timedelta(minutes = item.duration)
	return data
<commit_msg>fixed tabs<commit_after># -*- coding: utf-8 -*-
# Copyright (c) 2015, ESS LLP and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
import json
from frappe.utils import getdate
from frappe import _
import datetime
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account,get_income_account
from erpnext.hr.doctype.employee.employee import is_holiday

class PatientAppointment(Document):
	def on_update(self):
		today = datetime.date.today()
		appointment_date = getdate(self.appointment_date)

		# If appointment created for today set as open
		if today == appointment_date:
			frappe.db.set_value("Patient Appointment", self.name, "status", "Open")
			self.reload()

	def after_insert(self):
		# Check fee validity exists
		appointment = self
		validity_exist = validity_exists(appointment.physician, appointment.patient)
		if validity_exist:
			fee_validity = frappe.get_doc("Fee Validity", validity_exist[0][0])

			# Check if the validity is valid
			appointment_date = getdate(appointment.appointment_date)
			if (fee_validity.valid_till >= appointment_date) and (fee_validity.visited < fee_validity.max_visit):
				visited = fee_validity.visited + 1
				frappe.db.set_value("Fee Validity", fee_validity.name, "visited", visited)
				if fee_validity.ref_invoice:
					frappe.db.set_value("Patient Appointment", appointment.name, "sales_invoice", fee_validity.ref_invoice)
				frappe.msgprint(_("{0} has fee validity till {1}").format(appointment.patient, fee_validity.valid_till))
		confirm_sms(self)

	def create_invoice(self):
		return invoice_appointment(self)

def appointment_cancel(appointment_id):
	appointment = frappe.get_doc("Patient Appointment", appointment_id)

	# If invoice --> fee_validity update with -1 visit
	if appointment.sales_invoice:
		validity = frappe.db.exists({"doctype": "Fee Validity", "ref_invoice": appointment.sales_invoice})
		if validity:
			fee_validity = frappe.get_doc("Fee Validity", validity[0][0])
			visited = fee_validity.visited - 1
			frappe.db.set_value("Fee Validity", fee_validity.name, "visited", visited)
			if visited <= 0:
				frappe.msgprint(
					_("Appointment cancelled, Please review and cancel the invoice {0}".format(appointment.sales_invoice))
				)
			else:
				frappe.msgprint(_("Appointment cancelled"))

@frappe.whitelist()
def get_availability_data(date, physician):
	"""
	Get availability data of 'physician' on 'date'
	:param date: Date to check in schedule
	:param physician: Name of the physician
	:return: dict containing a list of available slots, list of appointments and time of appointments
	"""

	date = getdate(date)
	weekday = date.strftime("%A")

	available_slots = []
	slot_details = []
	physician_schedule = None

	employee = None

	physician_obj = frappe.get_doc("Physician", physician)

	# Get Physician employee relation
	if physician_obj.employee:
		employee = physician_obj.employee
	elif physician_obj.user_id:
		if frappe.db.exists({
			"doctype": "Employee",
			"user_id": physician_obj.user_id
			}):
			employee = frappe.get_doc("Employee", {"user_id": physician_obj.user_id}).name

	if employee:
		# Check if it is Holiday
		if is_holiday(employee, date):
			frappe.throw(_("{0} is a company holiday".format(date)))

		# Check if He/She on Leave
		leave_record = frappe.db.sql("""select half_day from `tabLeave Application`
			where employee = %s and %s between from_date and to_date
			and docstatus = 1""", (employee, date), as_dict=True)
		if leave_record:
			if leave_record[0].half_day:
				frappe.throw(_("Dr {0} on Half day Leave on {1}").format(physician, date))
			else:
				frappe.throw(_("Dr {0} on Leave on {1}").format(physician, date))

	# get physicians schedule
	if physician_obj.physician_schedules:
		for schedule in physician_obj.physician_schedules:
			if schedule.schedule:
				physician_schedule = frappe.get_doc("Physician Schedule", schedule.schedule)
			else:
				frappe.throw(_("Dr {0} does not have a Physician Schedule. Add it in Physician master".format(physician)))

			if physician_schedule:
				available_slots = []
				for t in physician_schedule.time_slots:
					if weekday == t.day:
						available_slots.append(t)

				if available_slots:
					appointments = []
				 	if schedule.service_unit:
						slot_name  = schedule.schedule+" - "+schedule.service_unit
						allow_overlap = frappe.get_value('Patient Service Unit', schedule.service_unit, 'overlap_appointments')
						if allow_overlap:
							# fetch all appointments to physician by service unit
							appointments = frappe.get_all(
								"Patient Appointment",
								filters={"physician": physician, "service_unit": schedule.service_unit, "appointment_date": date, "status": ["not in",["Cancelled"]]},
								fields=["name", "appointment_time", "duration", "status"])
						else:
							# fetch all appointments to service unit
							appointments = frappe.get_all(
								"Patient Appointment",
								filters={"service_unit": schedule.service_unit, "appointment_date": date, "status": ["not in",["Cancelled"]]},
								fields=["name", "appointment_time", "duration", "status"])
					else:
						slot_name = schedule.schedule
						# fetch all appointments to physician without service unit
						appointments = frappe.get_all(
							"Patient Appointment",
							filters={"physician": physician, "service_unit": '', "appointment_date": date, "status": ["not in",["Cancelled"]]},
							fields=["name", "appointment_time", "duration", "status"])

					slot_details.append({"slot_name":slot_name, "service_unit":schedule.service_unit,
						"avail_slot":available_slots, 'appointments': appointments})

	else:
		frappe.throw(_("Dr {0} does not have a Physician Schedule. Add it in Physician master".format(physician)))

	if not available_slots and not slot_details:
		# TODO: return available slots in nearby dates
		frappe.throw(_("Physician not available on {0}").format(weekday))

	return {
		"slot_details": slot_details
	}

@frappe.whitelist()
def update_status(appointment_id, status):
	frappe.db.set_value("Patient Appointment", appointment_id, "status", status)
	if status == "Cancelled":
		appointment_cancel(appointment_id)

@frappe.whitelist()
def set_open_appointments():
	today = getdate()
	frappe.db.sql(
		"update `tabPatient Appointment` set status='Open' where status = 'Scheduled'"
		" and appointment_date = %s", today)

@frappe.whitelist()
def set_pending_appointments():
	today = getdate()
	frappe.db.sql(
		"update `tabPatient Appointment` set status='Pending' where status in "
		"('Scheduled','Open') and appointment_date < %s", today)

def confirm_sms(doc):
	if frappe.db.get_value("Healthcare Settings", None, "app_con") == '1':
		message = frappe.db.get_value("Healthcare Settings", None, "app_con_msg")
		send_message(doc, message)

@frappe.whitelist()
def invoice_appointment(appointment_doc):
	if not appointment_doc.name:
		return False
	sales_invoice = frappe.new_doc("Sales Invoice")
	sales_invoice.customer = frappe.get_value("Patient", appointment_doc.patient, "customer")
	sales_invoice.appointment = appointment_doc.name
	sales_invoice.due_date = getdate()
	sales_invoice.is_pos = '0'
	sales_invoice.debit_to = get_receivable_account(appointment_doc.company)

	fee_validity = get_fee_validity(appointment_doc.physician, appointment_doc.patient, appointment_doc.appointment_date)
	create_invoice_items(appointment_doc.physician, appointment_doc.company, sales_invoice)

	sales_invoice.save(ignore_permissions=True)
	frappe.db.sql("""update `tabPatient Appointment` set sales_invoice=%s where name=%s""", (sales_invoice.name, appointment_doc.name))
	frappe.db.set_value("Fee Validity", fee_validity.name, "ref_invoice", sales_invoice.name)
	consultation = frappe.db.exists({
			"doctype": "Consultation",
			"appointment": appointment_doc.name})
	if consultation:
		frappe.db.set_value("Consultation", consultation[0][0], "invoice", sales_invoice.name)
	return sales_invoice.name

def get_fee_validity(physician, patient, date):
	validity_exist = validity_exists(physician, patient)
	if validity_exist:
		fee_validity = frappe.get_doc("Fee Validity", validity_exist[0][0])
		fee_validity = update_fee_validity(fee_validity, date)
	else:
		fee_validity = create_fee_validity(physician, patient, date)
	return fee_validity

def validity_exists(physician, patient):
	return frappe.db.exists({
			"doctype": "Fee Validity",
			"physician": physician,
			"patient": patient})

def update_fee_validity(fee_validity, date):
	max_visit = frappe.db.get_value("Healthcare Settings", None, "max_visit")
	valid_days = frappe.db.get_value("Healthcare Settings", None, "valid_days")
	if not valid_days:
		valid_days = 1
	if not max_visit:
		max_visit = 1
	date = getdate(date)
	valid_till = date + datetime.timedelta(days=int(valid_days))
	fee_validity.max_visit = max_visit
	fee_validity.visited = 1
	fee_validity.valid_till = valid_till
	fee_validity.save(ignore_permissions=True)
	return fee_validity

def create_fee_validity(physician, patient, date):
	fee_validity = frappe.new_doc("Fee Validity")
	fee_validity.physician = physician
	fee_validity.patient = patient
	fee_validity = update_fee_validity(fee_validity, date)
	return fee_validity

def create_invoice_items(physician, company, invoice):
	item_line = invoice.append("items")
	item_line.item_name = "Consulting Charges"
	item_line.description = "Consulting Charges:  " + physician
	item_line.qty = 1
	item_line.uom = "Nos"
	item_line.conversion_factor = 1
	item_line.income_account = get_income_account(physician, company)
	op_consulting_charge = frappe.db.get_value("Physician", physician, "op_consulting_charge")
	if op_consulting_charge:
		item_line.rate = op_consulting_charge
		item_line.amount = op_consulting_charge
	return invoice

@frappe.whitelist()
def create_consultation(appointment):
	appointment = frappe.get_doc("Patient Appointment", appointment)
	consultation = frappe.new_doc("Consultation")
	consultation.appointment = appointment.name
	consultation.patient = appointment.patient
	consultation.physician = appointment.physician
	consultation.visit_department = appointment.department
	consultation.patient_sex = appointment.patient_sex
	consultation.consultation_date = appointment.appointment_date
	if appointment.sales_invoice:
		consultation.invoice = appointment.sales_invoice
	return consultation.as_dict()

def remind_appointment():
	if frappe.db.get_value("Healthcare Settings", None, "app_rem") == '1':
		rem_before = datetime.datetime.strptime(frappe.get_value("Healthcare Settings", None, "rem_before"), "%H:%M:%S")
		rem_dt = datetime.datetime.now() + datetime.timedelta(
			hours=rem_before.hour, minutes=rem_before.minute, seconds=rem_before.second)

		appointment_list = frappe.db.sql(
			"select name from `tabPatient Appointment` where start_dt between %s and %s and reminded = 0 ",
			(datetime.datetime.now(), rem_dt)
		)

		for i in range(0, len(appointment_list)):
			doc = frappe.get_doc("Patient Appointment", appointment_list[i][0])
			message = frappe.db.get_value("Healthcare Settings", None, "app_rem_msg")
			send_message(doc, message)
			frappe.db.set_value("Patient Appointment", doc.name, "reminded",1)

def send_message(doc, message):
	patient = frappe.get_doc("Patient", doc.patient)
	if patient.mobile:
		context = {"doc": doc, "alert": doc, "comments": None}
		if doc.get("_comments"):
			context["comments"] = json.loads(doc.get("_comments"))

		# jinja to string convertion happens here
		message = frappe.render_template(message, context)
		number = [patient.mobile]
		send_sms(number, message)

@frappe.whitelist()
def get_events(start, end, filters=None):
	"""Returns events for Gantt / Calendar view rendering.

	:param start: Start date-time.
	:param end: End date-time.
	:param filters: Filters (JSON).
	"""
	from frappe.desk.calendar import get_event_conditions
	conditions = get_event_conditions("Patient Appointment", filters)
	data = frappe.db.sql("""select name, patient, physician, status,
		duration, timestamp(appointment_date, appointment_time) as
		'start' from `tabPatient Appointment` where
		(appointment_date between %(start)s and %(end)s)
		and docstatus < 2 {conditions}""".format(conditions=conditions),
		{"start": start, "end": end}, as_dict=True, update={"allDay": 0})
	for item in data:
		item.end = item.start + datetime.timedelta(minutes = item.duration)
	return data<|endoftext|>


<commit_before>import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext import declarative

Session = None
Base = declarative.declarative_base()

def init_model(engine):
    """Call me before using any of the tables or classes in the model."""
    global Session

    sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)
    Session = orm.scoped_session(sm)

commits_repositories = sa.Table(
    'commits_repositories',
    Base.metadata,
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True),
    sa.Column('repository_name',
              sa.types.String(length=40),
              sa.ForeignKey('repositories.name'),
              primary_key=True))

blobs_commits = sa.Table(
    'blobs_commits',
    Base.metadata,
    sa.Column('blob_name',
              sa.types.String(length=40),
              sa.ForeignKey('blobs.name'),
              primary_key=True),
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True))

trees_commits = sa.Table(
    'trees_commits',
    Base.metadata,
    sa.Column('tree_name',
              sa.types.String(length=40),
              sa.ForeignKey('trees.name'),
              primary_key=True),
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True))

class Repository(Base):
    __tablename__ = 'repositories'
    name = sa.Column(sa.types.String(length=40), primary_key=True)
    url = sa.Column(sa.types.String(length=255), unique=True)

class GitObject(Base):
    __tablename__ = 'git_objects'
    name = sa.Column(sa.types.String(length=40), primary_key=True)
    type = sa.Column(sa.types.String(length=50))

    __mapper_args__ = {'polymorphic_on': type}

class Commit(GitObject):
    __tablename__ = 'commits'
    __mapper_args__ = {'polymorphic_identity': 'commit'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    repositories = orm.relation(Repository,
                                backref=orm.backref('commits',
                                                    collection_class=set),
                                collection_class=set,
                                secondary=commits_repositories)

class Tree(GitObject):
    __tablename__ = 'trees'
    __mapper_args__ = {'polymorphic_identity': 'tree'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    commits = orm.relation(Commit,
                           backref=orm.backref('trees',
                                               collection_class=set),
                           collection_class=set,
                           secondary=trees_commits)

class Blob(GitObject):
    __tablename__ = 'blobs'
    __mapper_args__ = {'polymorphic_identity': 'blob'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    commits = orm.relation(Commit,
                           backref=orm.backref('blobs',
                                               collection_class=set),
                           collection_class=set,
                           secondary=blobs_commits)

class Tag(GitObject):
    __tablename__ = 'tags'
    __mapper_args__ = {'polymorphic_identity': 'tag'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.tag'),
                     primary_key=True)

    commit = orm.relation(Commit,
                          backref=orm.backref('tags',
                                              collection_class=set,
                                              cascade='all'))
<commit_msg>Expose the SQLAlchemy Metadata and Engine in the database module.<commit_after>import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext import declarative

Session = None
Engine = None
Base = declarative.declarative_base()
Metadata = Base.metadata

def init_model(engine):
    """Call me before using any of the tables or classes in the model."""
    global Session, Engine

    Engine = engine

    sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)
    Session = orm.scoped_session(sm)

commits_repositories = sa.Table(
    'commits_repositories',
    Base.metadata,
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True),
    sa.Column('repository_name',
              sa.types.String(length=40),
              sa.ForeignKey('repositories.name'),
              primary_key=True))

blobs_commits = sa.Table(
    'blobs_commits',
    Base.metadata,
    sa.Column('blob_name',
              sa.types.String(length=40),
              sa.ForeignKey('blobs.name'),
              primary_key=True),
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True))

trees_commits = sa.Table(
    'trees_commits',
    Base.metadata,
    sa.Column('tree_name',
              sa.types.String(length=40),
              sa.ForeignKey('trees.name'),
              primary_key=True),
    sa.Column('commit_name',
              sa.types.String(length=40),
              sa.ForeignKey('commits.name'),
              primary_key=True))

class Repository(Base):
    __tablename__ = 'repositories'
    name = sa.Column(sa.types.String(length=40), primary_key=True)
    url = sa.Column(sa.types.String(length=255), unique=True)

class GitObject(Base):
    __tablename__ = 'git_objects'
    name = sa.Column(sa.types.String(length=40), primary_key=True)
    type = sa.Column(sa.types.String(length=50))

    __mapper_args__ = {'polymorphic_on': type}

class Commit(GitObject):
    __tablename__ = 'commits'
    __mapper_args__ = {'polymorphic_identity': 'commit'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    repositories = orm.relation(Repository,
                                backref=orm.backref('commits',
                                                    collection_class=set),
                                collection_class=set,
                                secondary=commits_repositories)

class Tree(GitObject):
    __tablename__ = 'trees'
    __mapper_args__ = {'polymorphic_identity': 'tree'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    commits = orm.relation(Commit,
                           backref=orm.backref('trees',
                                               collection_class=set),
                           collection_class=set,
                           secondary=trees_commits)

class Blob(GitObject):
    __tablename__ = 'blobs'
    __mapper_args__ = {'polymorphic_identity': 'blob'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.name'),
                     primary_key=True)

    commits = orm.relation(Commit,
                           backref=orm.backref('blobs',
                                               collection_class=set),
                           collection_class=set,
                           secondary=blobs_commits)

class Tag(GitObject):
    __tablename__ = 'tags'
    __mapper_args__ = {'polymorphic_identity': 'tag'}
    name = sa.Column(sa.types.String(length=40),
                     sa.ForeignKey('git_objects.tag'),
                     primary_key=True)

    commit = orm.relation(Commit,
                          backref=orm.backref('tags',
                                              collection_class=set,
                                              cascade='all'))
<|endoftext|>