<commit_before># This file is part of Indico.
# Copyright (C) 2002 - 2017 European Organization for Nuclear Research (CERN).
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# Indico 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Indico; if not, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals

import shutil
from collections import defaultdict
from io import BytesIO

from flask import flash, jsonify, request, session
from PIL import Image
from werkzeug.exceptions import Forbidden

from indico.core.db import db
from indico.legacy.webinterface.rh.base import RHModificationBaseProtected, check_event_locked
from indico.modules.categories import Category
from indico.modules.categories.controllers.management import RHManageCategoryBase
from indico.modules.designer import DEFAULT_CONFIG, TemplateType
from indico.modules.designer.forms import AddTemplateForm
from indico.modules.designer.models.images import DesignerImageFile
from indico.modules.designer.models.templates import DesignerTemplate
from indico.modules.designer.operations import update_template
from indico.modules.designer.util import (get_all_templates, get_default_template_on_category, get_inherited_templates,
                                          get_not_deletable_templates, get_placeholder_options)
from indico.modules.designer.views import WPCategoryManagementDesigner, WPEventManagementDesigner
from indico.modules.events import Event
from indico.modules.events.management.controllers import RHManageEventBase
from indico.modules.events.registration.models.forms import RegistrationForm
from indico.util.fs import secure_filename
from indico.util.i18n import _
from indico.web.flask.templating import get_template_module
from indico.web.util import jsonify_data, jsonify_form, jsonify_template

TEMPLATE_DATA_JSON_SCHEMA = {
    'type': 'object',
    'properties': {
        'width': {'type': 'integer', 'minimum': 0},
        'height': {'type': 'integer', 'minimum': 0},
        'background_position': {'type': 'string'},
        'items': {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'id': {'type': 'integer'},
                    'text': {'type': 'string'},
                    'x': {'type': 'number', 'minimum': 0},
                    'y': {'type': 'number', 'minimum': 0},
                    'width': {'type': 'integer', 'minimum': 0},
                    'color': {'type': 'string'},
                    'font_family': {'type': 'string'},
                    'font_size': {'type': 'string'},
                    'text_align': {'type': 'string'},
                    'bold': {'type': 'boolean'},
                    'italic': {'type': 'boolean'}
                }
            },
            'required': ['id', 'text', 'x', 'y', 'width', 'color', 'font_family', 'font_size', 'text_align',
                         'bold', 'italic']
        },
    },
    'required': ['width', 'height', 'background_position', 'items']
}

def _render_template_list(target, event=None):
    tpl = get_template_module('designer/_list.html')
    default_template = get_default_template_on_category(target) if isinstance(target, Category) else None
    not_deletable = get_not_deletable_templates(target)
    return tpl.render_template_list(target.designer_templates, target, event=event, default_template=default_template,
                                    inherited_templates=get_inherited_templates(target),
                                    not_deletable_templates=not_deletable)

class TemplateDesignerMixin:
    """
    Basic class for all template designer mixins.

    It resolves the target object type from the blueprint URL.
    """
    @property
    def object_type(self):
        """Figure out whether we're targetting an event or category, based on URL info."""
        return request.view_args['object_type']

    @property
    def event_or_none(self):
        return self.target if self.object_type == 'event' else None

    def _render_template(self, tpl_name, **kwargs):
        view_class = WPEventManagementDesigner if self.object_type == 'event' else WPCategoryManagementDesigner
        return view_class.render_template(tpl_name, self.target, 'designer', target=self.target, **kwargs)

class SpecificTemplateMixin(TemplateDesignerMixin):
    """
    Mixin that accepts a target template passed in the URL.

    The target category/event will be the owner of that template.
    """
    normalize_url_spec = {
        'locators': {
            lambda self: self.template
        }
    }

    @property
    def target(self):
        return self.template.owner

    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden
        elif isinstance(self.target, Event):
            check_event_locked(self, self.target)

    def _checkParams(self):
        self.template = DesignerTemplate.get_one(request.view_args['template_id'])

class TargetFromURLMixin(TemplateDesignerMixin):
    """
    Mixin that takes the target event/category from the URL that is passed.
    """

    @property
    def target_dict(self):
        return {'event_new': self.event_new} if self.object_type == 'event' else {'category': self.category}

    @property
    def target(self):
        return self.event_new if self.object_type == 'event' else self.category

class TemplateListMixin(TargetFromURLMixin):
    def _process(self):
        templates = get_inherited_templates(self.target)
        not_deletable = get_not_deletable_templates(self.target)
        default_template = get_default_template_on_category(self.target) if isinstance(self.target, Category) else None
        return self._render_template('list.html', inherited_templates=templates, not_deletable_templates=not_deletable,
                                     default_template=default_template,)

class CloneTemplateMixin(TargetFromURLMixin):
    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden

    def _checkParams(self):
        self.template = DesignerTemplate.get_one(request.view_args['template_id'])

    def _process(self):
        title = "{} (copy)".format(self.template.title)
        new_template = DesignerTemplate(title=title, type=self.template.type, data=self.template.data,
                                        **self.target_dict)

        if self.template.background_image:
            background = self.template.background_image
            new_background = DesignerImageFile(filename=background.filename, content_type=background.content_type,
                                               template=new_template)
            with background.open() as f:
                new_background.save(f)
        else:
            new_background = None

        new_template.background_image = new_background

        flash(_("Created copy of template '{}'").format(self.template.title), 'success')
        return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))

class AddTemplateMixin(TargetFromURLMixin):
    always_clonable = False

    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden

    def _process(self):
        form = AddTemplateForm()
        if self.always_clonable:
            del form.is_clonable
        if form.validate_on_submit():
            is_clonable = form.is_clonable.data if form.is_clonable else True
            new_template = DesignerTemplate(title=form.title.data, type=form.type.data, is_clonable=is_clonable,
                                            **self.target_dict)
            flash(_("Added new template '{}'").format(new_template.title), 'success')
            return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))
        return jsonify_form(form, disabled_until_change=False)

class RHListEventTemplates(TemplateListMixin, RHManageEventBase):
    pass

class RHListCategoryTemplates(TemplateListMixin, RHManageCategoryBase):
    pass

class RHAddEventTemplate(AddTemplateMixin, RHManageEventBase):
    always_clonable = True

class RHAddCategoryTemplate(AddTemplateMixin, RHManageCategoryBase):
    always_clonable = False

class RHCloneEventTemplate(CloneTemplateMixin, RHManageEventBase):
    def _checkParams(self, params):
        RHManageEventBase._checkParams(self, params)
        CloneTemplateMixin._checkParams(self)

class RHCloneCategoryTemplate(CloneTemplateMixin, RHManageCategoryBase):
    def _checkParams(self, params):
        RHManageCategoryBase._checkParams(self)
        CloneTemplateMixin._checkParams(self)

class RHModifyDesignerTemplateBase(SpecificTemplateMixin, RHModificationBaseProtected):
    def _checkParams(self, params):
        RHModificationBaseProtected._checkParams(self, params)
        SpecificTemplateMixin._checkParams(self)

class RHEditDesignerTemplate(RHModifyDesignerTemplateBase):
    def _process_GET(self):
        bs_template = self.template.backside_template
        template_data = {
            'title': self.template.title,
            'data': self.template.data,
            'is_clonable': self.template.is_clonable,
            'background_url': self.template.background_image.download_url if self.template.background_image else None
        }
        backside_template_data = {
            'id': bs_template.id if bs_template else None,
            'title': bs_template.title if bs_template else None,
            'data': bs_template.data if bs_template else None,
            'background_url': (bs_template.background_image.download_url
                               if bs_template and bs_template.background_image else None)
        }
        backside_templates = (DesignerTemplate.query
                              .filter(DesignerTemplate.backside_template_id == self.template.id)
                              .all())
        related_tpls_per_owner = defaultdict(list)
        for bs_tpl in backside_templates:
            related_tpls_per_owner[bs_tpl.owner].append(bs_tpl)
        return self._render_template('template.html', template=self.template, placeholders=get_placeholder_options(),
                                     config=DEFAULT_CONFIG[self.template.type], owner=self.target,
                                     template_data=template_data, backside_template_data=backside_template_data,
                                     related_tpls_per_owner=related_tpls_per_owner, tpls_count=len(backside_templates))

    def _process_POST(self):
        data = dict({'background_position': 'stretch', 'items': []}, **request.json['template'])
        self.validate_json(TEMPLATE_DATA_JSON_SCHEMA, data)
        update_template(self.template, title=request.json['title'], data=data,
                        backside_template_id=request.json['backside_template_id'],
                        is_clonable=request.json['is_clonable'],
                        clear_background=request.json['clear_background'])
        flash(_("Template successfully saved."), 'success')
        return jsonify_data()

class RHDownloadTemplateImage(RHModifyDesignerTemplateBase):
    normalize_url_spec = {
        'locators': {
            lambda self: self.image
        }
    }

    def _checkParams(self, params):
        RHModifyDesignerTemplateBase._checkParams(self, params)
        self.image = DesignerImageFile.find_one(id=request.view_args['image_id'], template=self.template)

    def _process(self):
        return self.image.send()

class RHUploadBackgroundImage(RHModifyDesignerTemplateBase):
    def _process(self):
        f = request.files['file']
        filename = secure_filename(f.filename, 'image')
        data = BytesIO()
        shutil.copyfileobj(f, data)
        data.seek(0)
        try:
            image_type = Image.open(data).format.lower()
        except IOError:
            # Invalid image data
            return jsonify(error="Invalid image data!")
        data.seek(0)
        if image_type not in {'jpeg', 'gif', 'png'}:
            return jsonify(error="File format not accepted!")
        content_type = 'image/' + image_type
        image = DesignerImageFile(template=self.template, filename=filename, content_type=content_type)
        self.template.background_image = image
        image.save(data)
        flash(_("The image has been uploaded"), 'success')
        return jsonify_data(image_url=image.download_url)

class RHDeleteDesignerTemplate(RHModifyDesignerTemplateBase):
    def _process(self):
        affected_regforms = RegistrationForm.find_all(ticket_template_id=self.template.id)
        for regform in affected_regforms:
            regform.ticket_template = None
        affected_categories = Category.find_all(default_ticket_template_id=self.template.id)
        for category in affected_categories:
            category.default_ticket_template = None
            if category.is_root:
                category.default_ticket_template = DesignerTemplate.find_first(DesignerTemplate.system_template)
        db.session.delete(self.template)
        flash(_('The template has been removed'), 'success')
        return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))

class RHListBacksideTemplates(RHModifyDesignerTemplateBase, RHListEventTemplates):
    def _process(self):
        inherited_templates = [tpl for tpl in get_inherited_templates(self.target)
                               if not tpl.backside_template and tpl.type == TemplateType.badge]
        custom_templates = [tpl for tpl in self.target.designer_templates
                            if not tpl.backside_template and tpl != self.template]
        return jsonify_template('designer/backside_list.html', target=self.target, custom_templates=custom_templates,
                                inherited_templates=inherited_templates, current_template=self.template,
                                width=int(request.args['width']), height=int(request.args['height']))

class RHGetTemplateData(RHModifyDesignerTemplateBase):
    def _process(self):
        template_data = {
            'title': self.template.title,
            'data': self.template.data,
            'background_url': self.template.background_image.download_url if self.template.background_image else None
        }
        return jsonify(template=template_data, backside_template_id=self.template.id)

class RHToggleTemplateDefaultOnCategory(RHManageCategoryBase):
    def _process(self):
        template = DesignerTemplate.get_one(request.view_args['template_id'])
        if template not in get_all_templates(self.category):
            raise Exception('Invalid template')
        if template == self.category.default_ticket_template:
            # already the default -> clear it
            self.category.default_ticket_template = None
        elif template == get_default_template_on_category(self.category, only_inherited=True):
            # already the inherited default -> clear it instead of setting it explicitly
            self.category.default_ticket_template = None
        else:
            self.category.default_ticket_template = template
        if self.category.is_root and not self.category.default_ticket_template:
            raise Exception('Cannot clear default ticket template on root category')
        return jsonify_data(html=_render_template_list(self.category))
<commit_msg>Fix/simplify designer template deletion<commit_after># This file is part of Indico.
# Copyright (C) 2002 - 2017 European Organization for Nuclear Research (CERN).
#
# Indico is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.
#
# Indico 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
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Indico; if not, see <http://www.gnu.org/licenses/>.

from __future__ import unicode_literals

import shutil
from collections import defaultdict
from io import BytesIO

from flask import flash, jsonify, request, session
from PIL import Image
from werkzeug.exceptions import Forbidden

from indico.core.db import db
from indico.legacy.webinterface.rh.base import RHModificationBaseProtected, check_event_locked
from indico.modules.categories import Category
from indico.modules.categories.controllers.management import RHManageCategoryBase
from indico.modules.designer import DEFAULT_CONFIG, TemplateType
from indico.modules.designer.forms import AddTemplateForm
from indico.modules.designer.models.images import DesignerImageFile
from indico.modules.designer.models.templates import DesignerTemplate
from indico.modules.designer.operations import update_template
from indico.modules.designer.util import (get_all_templates, get_default_template_on_category, get_inherited_templates,
                                          get_not_deletable_templates, get_placeholder_options)
from indico.modules.designer.views import WPCategoryManagementDesigner, WPEventManagementDesigner
from indico.modules.events import Event
from indico.modules.events.management.controllers import RHManageEventBase
from indico.util.fs import secure_filename
from indico.util.i18n import _
from indico.web.flask.templating import get_template_module
from indico.web.util import jsonify_data, jsonify_form, jsonify_template

TEMPLATE_DATA_JSON_SCHEMA = {
    'type': 'object',
    'properties': {
        'width': {'type': 'integer', 'minimum': 0},
        'height': {'type': 'integer', 'minimum': 0},
        'background_position': {'type': 'string'},
        'items': {
            'type': 'array',
            'items': {
                'type': 'object',
                'properties': {
                    'id': {'type': 'integer'},
                    'text': {'type': 'string'},
                    'x': {'type': 'number', 'minimum': 0},
                    'y': {'type': 'number', 'minimum': 0},
                    'width': {'type': 'integer', 'minimum': 0},
                    'color': {'type': 'string'},
                    'font_family': {'type': 'string'},
                    'font_size': {'type': 'string'},
                    'text_align': {'type': 'string'},
                    'bold': {'type': 'boolean'},
                    'italic': {'type': 'boolean'}
                }
            },
            'required': ['id', 'text', 'x', 'y', 'width', 'color', 'font_family', 'font_size', 'text_align',
                         'bold', 'italic']
        },
    },
    'required': ['width', 'height', 'background_position', 'items']
}

def _render_template_list(target, event=None):
    tpl = get_template_module('designer/_list.html')
    default_template = get_default_template_on_category(target) if isinstance(target, Category) else None
    not_deletable = get_not_deletable_templates(target)
    return tpl.render_template_list(target.designer_templates, target, event=event, default_template=default_template,
                                    inherited_templates=get_inherited_templates(target),
                                    not_deletable_templates=not_deletable)

class TemplateDesignerMixin:
    """
    Basic class for all template designer mixins.

    It resolves the target object type from the blueprint URL.
    """
    @property
    def object_type(self):
        """Figure out whether we're targetting an event or category, based on URL info."""
        return request.view_args['object_type']

    @property
    def event_or_none(self):
        return self.target if self.object_type == 'event' else None

    def _render_template(self, tpl_name, **kwargs):
        view_class = WPEventManagementDesigner if self.object_type == 'event' else WPCategoryManagementDesigner
        return view_class.render_template(tpl_name, self.target, 'designer', target=self.target, **kwargs)

class SpecificTemplateMixin(TemplateDesignerMixin):
    """
    Mixin that accepts a target template passed in the URL.

    The target category/event will be the owner of that template.
    """
    normalize_url_spec = {
        'locators': {
            lambda self: self.template
        }
    }

    @property
    def target(self):
        return self.template.owner

    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden
        elif isinstance(self.target, Event):
            check_event_locked(self, self.target)

    def _checkParams(self):
        self.template = DesignerTemplate.get_one(request.view_args['template_id'])

class TargetFromURLMixin(TemplateDesignerMixin):
    """
    Mixin that takes the target event/category from the URL that is passed.
    """

    @property
    def target_dict(self):
        return {'event_new': self.event_new} if self.object_type == 'event' else {'category': self.category}

    @property
    def target(self):
        return self.event_new if self.object_type == 'event' else self.category

class TemplateListMixin(TargetFromURLMixin):
    def _process(self):
        templates = get_inherited_templates(self.target)
        not_deletable = get_not_deletable_templates(self.target)
        default_template = get_default_template_on_category(self.target) if isinstance(self.target, Category) else None
        return self._render_template('list.html', inherited_templates=templates, not_deletable_templates=not_deletable,
                                     default_template=default_template,)

class CloneTemplateMixin(TargetFromURLMixin):
    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden

    def _checkParams(self):
        self.template = DesignerTemplate.get_one(request.view_args['template_id'])

    def _process(self):
        title = "{} (copy)".format(self.template.title)
        new_template = DesignerTemplate(title=title, type=self.template.type, data=self.template.data,
                                        **self.target_dict)

        if self.template.background_image:
            background = self.template.background_image
            new_background = DesignerImageFile(filename=background.filename, content_type=background.content_type,
                                               template=new_template)
            with background.open() as f:
                new_background.save(f)
        else:
            new_background = None

        new_template.background_image = new_background

        flash(_("Created copy of template '{}'").format(self.template.title), 'success')
        return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))

class AddTemplateMixin(TargetFromURLMixin):
    always_clonable = False

    def _checkProtection(self):
        if not self.target.can_manage(session.user):
            raise Forbidden

    def _process(self):
        form = AddTemplateForm()
        if self.always_clonable:
            del form.is_clonable
        if form.validate_on_submit():
            is_clonable = form.is_clonable.data if form.is_clonable else True
            new_template = DesignerTemplate(title=form.title.data, type=form.type.data, is_clonable=is_clonable,
                                            **self.target_dict)
            flash(_("Added new template '{}'").format(new_template.title), 'success')
            return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))
        return jsonify_form(form, disabled_until_change=False)

class RHListEventTemplates(TemplateListMixin, RHManageEventBase):
    pass

class RHListCategoryTemplates(TemplateListMixin, RHManageCategoryBase):
    pass

class RHAddEventTemplate(AddTemplateMixin, RHManageEventBase):
    always_clonable = True

class RHAddCategoryTemplate(AddTemplateMixin, RHManageCategoryBase):
    always_clonable = False

class RHCloneEventTemplate(CloneTemplateMixin, RHManageEventBase):
    def _checkParams(self, params):
        RHManageEventBase._checkParams(self, params)
        CloneTemplateMixin._checkParams(self)

class RHCloneCategoryTemplate(CloneTemplateMixin, RHManageCategoryBase):
    def _checkParams(self, params):
        RHManageCategoryBase._checkParams(self)
        CloneTemplateMixin._checkParams(self)

class RHModifyDesignerTemplateBase(SpecificTemplateMixin, RHModificationBaseProtected):
    def _checkParams(self, params):
        RHModificationBaseProtected._checkParams(self, params)
        SpecificTemplateMixin._checkParams(self)

class RHEditDesignerTemplate(RHModifyDesignerTemplateBase):
    def _process_GET(self):
        bs_template = self.template.backside_template
        template_data = {
            'title': self.template.title,
            'data': self.template.data,
            'is_clonable': self.template.is_clonable,
            'background_url': self.template.background_image.download_url if self.template.background_image else None
        }
        backside_template_data = {
            'id': bs_template.id if bs_template else None,
            'title': bs_template.title if bs_template else None,
            'data': bs_template.data if bs_template else None,
            'background_url': (bs_template.background_image.download_url
                               if bs_template and bs_template.background_image else None)
        }
        backside_templates = (DesignerTemplate.query
                              .filter(DesignerTemplate.backside_template_id == self.template.id)
                              .all())
        related_tpls_per_owner = defaultdict(list)
        for bs_tpl in backside_templates:
            related_tpls_per_owner[bs_tpl.owner].append(bs_tpl)
        return self._render_template('template.html', template=self.template, placeholders=get_placeholder_options(),
                                     config=DEFAULT_CONFIG[self.template.type], owner=self.target,
                                     template_data=template_data, backside_template_data=backside_template_data,
                                     related_tpls_per_owner=related_tpls_per_owner, tpls_count=len(backside_templates))

    def _process_POST(self):
        data = dict({'background_position': 'stretch', 'items': []}, **request.json['template'])
        self.validate_json(TEMPLATE_DATA_JSON_SCHEMA, data)
        update_template(self.template, title=request.json['title'], data=data,
                        backside_template_id=request.json['backside_template_id'],
                        is_clonable=request.json['is_clonable'],
                        clear_background=request.json['clear_background'])
        flash(_("Template successfully saved."), 'success')
        return jsonify_data()

class RHDownloadTemplateImage(RHModifyDesignerTemplateBase):
    normalize_url_spec = {
        'locators': {
            lambda self: self.image
        }
    }

    def _checkParams(self, params):
        RHModifyDesignerTemplateBase._checkParams(self, params)
        self.image = DesignerImageFile.find_one(id=request.view_args['image_id'], template=self.template)

    def _process(self):
        return self.image.send()

class RHUploadBackgroundImage(RHModifyDesignerTemplateBase):
    def _process(self):
        f = request.files['file']
        filename = secure_filename(f.filename, 'image')
        data = BytesIO()
        shutil.copyfileobj(f, data)
        data.seek(0)
        try:
            image_type = Image.open(data).format.lower()
        except IOError:
            # Invalid image data
            return jsonify(error="Invalid image data!")
        data.seek(0)
        if image_type not in {'jpeg', 'gif', 'png'}:
            return jsonify(error="File format not accepted!")
        content_type = 'image/' + image_type
        image = DesignerImageFile(template=self.template, filename=filename, content_type=content_type)
        self.template.background_image = image
        image.save(data)
        flash(_("The image has been uploaded"), 'success')
        return jsonify_data(image_url=image.download_url)

class RHDeleteDesignerTemplate(RHModifyDesignerTemplateBase):
    def _process(self):
        db.session.delete(self.template)
        root = Category.get_root()
        if not root.default_ticket_template:
            # if we deleted the root category's default template, pick
            # a system template as the new default (this always exists)
            system_template = DesignerTemplate.find_first(DesignerTemplate.is_system_template,
                                                          DesignerTemplate.type == TemplateType.badge)
            root.default_ticket_template = system_template
        db.session.flush()
        flash(_('The template has been deleted'), 'success')
        return jsonify_data(html=_render_template_list(self.target, event=self.event_or_none))

class RHListBacksideTemplates(RHModifyDesignerTemplateBase, RHListEventTemplates):
    def _process(self):
        inherited_templates = [tpl for tpl in get_inherited_templates(self.target)
                               if not tpl.backside_template and tpl.type == TemplateType.badge]
        custom_templates = [tpl for tpl in self.target.designer_templates
                            if not tpl.backside_template and tpl != self.template]
        return jsonify_template('designer/backside_list.html', target=self.target, custom_templates=custom_templates,
                                inherited_templates=inherited_templates, current_template=self.template,
                                width=int(request.args['width']), height=int(request.args['height']))

class RHGetTemplateData(RHModifyDesignerTemplateBase):
    def _process(self):
        template_data = {
            'title': self.template.title,
            'data': self.template.data,
            'background_url': self.template.background_image.download_url if self.template.background_image else None
        }
        return jsonify(template=template_data, backside_template_id=self.template.id)

class RHToggleTemplateDefaultOnCategory(RHManageCategoryBase):
    def _process(self):
        template = DesignerTemplate.get_one(request.view_args['template_id'])
        if template not in get_all_templates(self.category):
            raise Exception('Invalid template')
        if template == self.category.default_ticket_template:
            # already the default -> clear it
            self.category.default_ticket_template = None
        elif template == get_default_template_on_category(self.category, only_inherited=True):
            # already the inherited default -> clear it instead of setting it explicitly
            self.category.default_ticket_template = None
        else:
            self.category.default_ticket_template = template
        if self.category.is_root and not self.category.default_ticket_template:
            raise Exception('Cannot clear default ticket template on root category')
        return jsonify_data(html=_render_template_list(self.category))
<|endoftext|>


<commit_before># Copyright 2013,2014 Music Technology Group - Universitat Pompeu Fabra
#
# This file is part of Dunya
#
# Dunya 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 (FSF), either version 3 of the License, or (at your option) any later
# version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see http://www.gnu.org/licenses/

from __future__ import print_function
from __future__ import absolute_import

import os
import traceback

import celery
import compmusic

import data
import docserver
import docserver.util
from dashboard import andalusian_importer
from dashboard import carnatic_importer
from dashboard import hindustani_importer
from dashboard import makam_importer
from dashboard import jingju_importer
from dashboard import models
from dashboard.log import import_logger
from dunya.celery import app

class DunyaTask(celery.Task):
    abstract = True

    def on_failure(self, exc, task_id, args, kwargs, einfo):
        print("got an exception")
        print(einfo)
        try:
            theobj = self.get_object(args[0])
            theobj.add_log_message(einfo)
            theobj.set_state_error()
        except self.ObjectClass.DoesNotExist:
            classname = "%s.%s" % (self.ObjectClass.__module__, self.ObjectClass.__name__)
            raise Exception("Cannot find %s object with key %s to add error to" % (classname, args[0]))
        raise exc

class CollectionDunyaTask(DunyaTask):
    ObjectClass = models.Collection

    def get_object(self, key):
        return self.ObjectClass.objects.get(collectionid=key)

class ReleaseDunyaTask(DunyaTask):
    ObjectClass = models.MusicbrainzRelease

    def get_object(self, key):
        return self.ObjectClass.objects.get(pk=key)

def force_load_and_import_collection(collectionid):
    """ Scan the collection contents and import every release again
    (including ones already imported)
    """
    # Note, use 'immutable subtasks' (.si()) which doesn't pass results from 1 method to the next
    chain = load_musicbrainz_collection.si(collectionid) | force_import_all_releases.si(collectionid)
    chain.apply_async()

@app.task(ignore_result=True)
def rematch_unknown_directory(collectiondirectory_id):
    """ Try and match a CollectionDirectory to a release in the collection. """
    cd = models.CollectionDirectory.objects.get(pk=collectiondirectory_id)
    _match_directory_to_release(cd.collection.collectionid, cd.full_path)

@app.task(base=CollectionDunyaTask)
def load_musicbrainz_collection(collectionid):
    """ Load a musicbrainz collection into the dashboard database
        and scan collection root to match directories to releases.
    """
    coll = models.Collection.objects.get(collectionid=collectionid)
    coll.set_state_scanning()
    coll.add_log_message("Starting collection scan")

    update_collection(collectionid)
    scan_and_link(collectionid)

    coll.set_state_scanned()
    coll.add_log_message("Collection scan finished")
    return collectionid

@app.task(base=ReleaseDunyaTask)
def import_single_release(releasepk):
    """ A job to find a release's collection, create a release importer,
    and then call import_release
    """
    release = models.MusicbrainzRelease.objects.get(pk=releasepk)
    collection = release.collection

    ri = get_release_importer(collection)
    if not ri:
        release.add_log_message("Cannot discover importer based on collection name (does it include carnatic/hindustani/makam/andalusian?)")
        release.set_state_error()
        return
    import_release(releasepk, ri)

def import_release(releasepk, ri):
    """ Import a single release into the database.
    Arguments:
    releasepk: a models.MusicbrainzRelease object PK
    ri: a release_importer object specific to this collection
    """
    release = models.MusicbrainzRelease.objects.get(pk=releasepk)
    original_ignore = release.ignore
    release.set_state_importing()
    release.add_log_message("Starting import")
    collection = release.collection

    # If there have been no errors, import to the docserver
    # and to dunya
    cfiles = models.CollectionFile.objects.filter(directory__musicbrainzrelease=release)
    for cfile in cfiles:
        # 3a. Import file to docserver
        docserver.util.docserver_add_mp3(collection.collectionid, release.mbid, cfile.path, cfile.recordingid)
        cfile.set_state_finished()

    # 3b. Import release to dunya database
    abort = False
    if collection.do_import:
        try:
            directories = [os.path.join(collection.audio_directory, d.path) for d in release.collectiondirectory_set.all()]
            # Set the release logger releaseid to the id of the current release
            import_logger.releaseid = release.pk
            ri.import_release(release.mbid, directories)
        except Exception:
            abort = True
            tb = traceback.format_exc()
            release.add_log_message(tb)

    if not abort:
        # If the import succeeded, clean up.
        release.add_log_message("Release import finished")
        release.set_state_finished()
        # If the release was ignored and we've successfully imported it, unignore
        if original_ignore:
            release.ignore = False
            release.save()
    else:
        # 3c. If there was an error, set this release as "ignored"
        release.ignore = True
        release.save()
        release.add_log_message("Release import aborted due to failure")
        release.set_state_error()

def get_release_importer(collection):
    name = collection.name.lower()
    name = name.lower()
    data_coll = data.models.Collection.objects.get(collectionid=collection.collectionid)
    ri = None
    if "hindustani" in name:
        ri = hindustani_importer.HindustaniReleaseImporter(data_coll)
    elif "carnatic" in name:
        ri = carnatic_importer.CarnaticReleaseImporter(data_coll)
    elif "makam" in name:
        ri = makam_importer.MakamReleaseImporter(data_coll)
    elif "andalusian" in name:
        ri = andalusian_importer.AndalusianReleaseImporter(data_coll)
    elif "jingju" in name:
        ri = jingju_importer.JingjuReleaseImporter(data_coll)
    return ri

@app.task(base=CollectionDunyaTask)
def force_import_all_releases(collectionid):
    """ Reimport releases in a dashboard collection.
    This will (re)import all releases that are in the collection, even if they
    are marked as ignored or are finished, unless they are missing a directory
    """
    collection = models.Collection.objects.get(collectionid=collectionid)
    ri = get_release_importer(collection)
    if not ri:
        collection.add_log_message("Cannot discover importer based on collection name (does it include carnatic/hindustani/makam/andalusian?)")
        collection.set_state_error()
        return
    collection.set_state_importing()
    # unlike the non-force version, we select all releases, not
    # only ones that haven't been ignored
    releases = collection.musicbrainzrelease_set.all()
    unstarted = []
    for r in releases:
        matched_paths = r.collectiondirectory_set.all()
        if len(matched_paths):
            if r.ignore:
                r.ignore = False
                r.save()
            unstarted.append(r)
    for r in unstarted:
        import_release(r.id, ri)
    collection.set_state_finished()

def _get_musicbrainz_release_for_dir(dirname):
    """ Get a unique list of all the musicbrainz release IDs that
    are in tags in mp3 files in the given directory.
    """
    release_ids = set()
    for fname in _get_mp3_files(os.listdir(dirname)):
        fpath = os.path.join(dirname, fname)
        meta = compmusic.file_metadata(fpath)
        if meta:
            rel = meta["meta"]["releaseid"]
            if rel:
                release_ids.add(rel)
    return list(release_ids)

def _get_mp3_files(files):
    """ Take a list of files and return only the mp3 files """
    # TODO: This should be any audio file, replace with util method
    return [f for f in files if os.path.splitext(f)[1].lower() == ".mp3"]

def update_collection(collectionid):
    """ Sync the contents of a collection on musicbrainz.org to our local
        database.

        If new releases have been added, put them in our local database.
        If releases have been removed, remove referenes to them in our
        database.
    """
    found_releases = compmusic.get_releases_in_collection(collectionid)
    coll = models.Collection.objects.get(collectionid=collectionid)
    existing_releases = [str(r.mbid) for r in coll.musicbrainzrelease_set.all()]
    to_remove = set(existing_releases) - set(found_releases)
    to_add = set(found_releases) - set(existing_releases)

    for relid in to_add:
        try:
            mbrelease = compmusic.mb.get_release_by_id(relid, includes=["artists"])["release"]
            title = mbrelease["title"]
            artist = mbrelease["artist-credit-phrase"]
            rel, created = models.MusicbrainzRelease.objects.get_or_create(
                mbid=relid,
                collection=coll,
                defaults={"title": title, "artist": artist})
        except compmusic.mb.ResponseError:
            coll.add_log_message("The collection had an entry for %s but I can't find a release with that ID" % relid)

    for relid in to_remove:
        coll.musicbrainzrelease_set.filter(mbid=relid).delete()

def _match_directory_to_release(collectionid, root):
    """ Try and match a single directory containing audio files to a release
        that exists in the given collection.

        collectionid: the ID of the collection we want the directory to be in
        root: the root path of the directory containing audio files
    """
    coll = models.Collection.objects.get(collectionid=collectionid)
    collectionroot = coll.audio_directory
    # Remove the path to the collection from the path to the files (we only store relative paths)
    if not collectionroot.endswith("/"):
        collectionroot += "/"
    if root.startswith(collectionroot):
        # This should always be true since we scan from the collection root
        shortpath = root[len(collectionroot):]
    else:
        shortpath = root
    # Try and find the musicbrainz release for the files in the directory
    cd, created = models.CollectionDirectory.objects.get_or_create(collection=coll, path=shortpath)
    rels = _get_musicbrainz_release_for_dir(root)
    if len(rels) == 1:
        releaseid = rels[0]
        try:
            therelease = models.MusicbrainzRelease.objects.get(mbid=releaseid, collection=coll)
            cd.musicbrainzrelease = therelease
            cd.save()

            mp3files = _get_mp3_files(os.listdir(root))
            for f in mp3files:
                _create_collectionfile(cd, f)
        except models.MusicbrainzRelease.DoesNotExist:
            pass

def _create_collectionfile(cd, name):
    """arguments:
       cd: a collectiondirectory
       name: the name of the file, with no path information
    """
    path = os.path.join(cd.full_path, name)
    meta = compmusic.file_metadata(path)
    recordingid = meta["meta"].get("recordingid")
    size = os.path.getsize(path)
    cfile, created = models.CollectionFile.objects.get_or_create(name=name, directory=cd, recordingid=recordingid, defaults={'filesize': size})
    if not created:
        cfile.filesize = size
        cfile.save()

def scan_and_link(collectionid):
    """ Scan the root directory of a collection and see if any directories
    have been added or removed.

    If they've been removed, delete references to them. If they've been
    added, scan the files for a musicbrainz release id and match them
    to musicbrainz releases that are part of the collection.
    """

    coll = models.Collection.objects.get(collectionid=collectionid)
    collectionroot = coll.audio_directory
    if not collectionroot.endswith("/"):
        collectionroot += "/"
    found_directories = []
    for root, d, files in os.walk(collectionroot):
        mp3files = _get_mp3_files(files)
        if len(mp3files) > 0:
            found_directories.append(root)
    existing_directories = [c.full_path for c in coll.collectiondirectory_set.all()]

    to_remove = set(existing_directories) - set(found_directories)
    to_add = set(found_directories) - set(existing_directories)

    for d in to_add:
        _match_directory_to_release(collectionid, d)

    for d in to_remove:
        # We have a full path, but collectiondirectories are
        # stored with a partial path, so cut it.
        shortpath = d[len(collectionroot):]
        coll.collectiondirectory_set.get(path=shortpath).delete()

    # For every CollectionDirectory that isn't matched to a MusicbrainzRelease,
    # try and match it
    cds = coll.collectiondirectory_set.filter(musicbrainzrelease__isnull=True)
    for cd in cds:
        _match_directory_to_release(coll.collectionid, cd.full_path)

    _check_existing_directories(coll)

def _check_existing_directories(coll):
    # For all of the matched directories, look at the contents of them and
    # remove/add files as needed
    # We don't remove from the docserver because it's not very important to
    # remove them, and it's complex to cover all cases. We do this separately
    for cd in coll.collectiondirectory_set.all():
        files = os.listdir(cd.full_path)
        mp3files = _get_mp3_files(files)
        existing_f = cd.collectionfile_set.all()
        existing_names = [f.name for f in existing_f]

        to_remove = set(existing_names) - set(mp3files)
        to_add = set(mp3files) - set(existing_names)
        same_files = set(mp3files) & set(existing_names)

        for rm in to_remove:
            # If it was renamed, we will make a CollectionFile in
            # the to_add block below
            cd.collectionfile_set.get(name=rm).delete()

        for f in same_files:
            # Look through all files and see if the mbid has changed. If it's changed,
            # update the reference
            fileobject = cd.collectionfile_set.get(name=f)
            meta = compmusic.file_metadata(os.path.join(cd.full_path, f))
            recordingid = meta["meta"].get("recordingid")
            if recordingid != fileobject.recordingid:
                fileobject.recordingid = recordingid
                fileobject.save()

        if to_add:
            # If there are new files in the directory, just run _match again and
            # it will create the new file objects
            _match_directory_to_release(coll.collectionid, cd.full_path)

@app.task
def delete_collection(collectionpk):
    collection = models.Collection.objects.get(pk=collectionpk)
    collection.delete()
<commit_msg>Temporary solution to avoid unrequired files in the import of a collection<commit_after># Copyright 2013,2014 Music Technology Group - Universitat Pompeu Fabra
#
# This file is part of Dunya
#
# Dunya 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 (FSF), either version 3 of the License, or (at your option) any later
# version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see http://www.gnu.org/licenses/

from __future__ import print_function
from __future__ import absolute_import

import os
import traceback

import celery
import compmusic

import data
import docserver
import docserver.util
from dashboard import andalusian_importer
from dashboard import carnatic_importer
from dashboard import hindustani_importer
from dashboard import makam_importer
from dashboard import jingju_importer
from dashboard import models
from dashboard.log import import_logger
from dunya.celery import app

class DunyaTask(celery.Task):
    abstract = True

    def on_failure(self, exc, task_id, args, kwargs, einfo):
        print("got an exception")
        print(einfo)
        try:
            theobj = self.get_object(args[0])
            theobj.add_log_message(einfo)
            theobj.set_state_error()
        except self.ObjectClass.DoesNotExist:
            classname = "%s.%s" % (self.ObjectClass.__module__, self.ObjectClass.__name__)
            raise Exception("Cannot find %s object with key %s to add error to" % (classname, args[0]))
        raise exc

class CollectionDunyaTask(DunyaTask):
    ObjectClass = models.Collection

    def get_object(self, key):
        return self.ObjectClass.objects.get(collectionid=key)

class ReleaseDunyaTask(DunyaTask):
    ObjectClass = models.MusicbrainzRelease

    def get_object(self, key):
        return self.ObjectClass.objects.get(pk=key)

def force_load_and_import_collection(collectionid):
    """ Scan the collection contents and import every release again
    (including ones already imported)
    """
    # Note, use 'immutable subtasks' (.si()) which doesn't pass results from 1 method to the next
    chain = load_musicbrainz_collection.si(collectionid) | force_import_all_releases.si(collectionid)
    chain.apply_async()

@app.task(ignore_result=True)
def rematch_unknown_directory(collectiondirectory_id):
    """ Try and match a CollectionDirectory to a release in the collection. """
    cd = models.CollectionDirectory.objects.get(pk=collectiondirectory_id)
    _match_directory_to_release(cd.collection.collectionid, cd.full_path)

@app.task(base=CollectionDunyaTask)
def load_musicbrainz_collection(collectionid):
    """ Load a musicbrainz collection into the dashboard database
        and scan collection root to match directories to releases.
    """
    coll = models.Collection.objects.get(collectionid=collectionid)
    coll.set_state_scanning()
    coll.add_log_message("Starting collection scan")

    update_collection(collectionid)
    scan_and_link(collectionid)

    coll.set_state_scanned()
    coll.add_log_message("Collection scan finished")
    return collectionid

@app.task(base=ReleaseDunyaTask)
def import_single_release(releasepk):
    """ A job to find a release's collection, create a release importer,
    and then call import_release
    """
    release = models.MusicbrainzRelease.objects.get(pk=releasepk)
    collection = release.collection

    ri = get_release_importer(collection)
    if not ri:
        release.add_log_message("Cannot discover importer based on collection name (does it include carnatic/hindustani/makam/andalusian?)")
        release.set_state_error()
        return
    import_release(releasepk, ri)

def import_release(releasepk, ri):
    """ Import a single release into the database.
    Arguments:
    releasepk: a models.MusicbrainzRelease object PK
    ri: a release_importer object specific to this collection
    """
    release = models.MusicbrainzRelease.objects.get(pk=releasepk)
    original_ignore = release.ignore
    release.set_state_importing()
    release.add_log_message("Starting import")
    collection = release.collection

    # If there have been no errors, import to the docserver
    # and to dunya
    cfiles = models.CollectionFile.objects.filter(directory__musicbrainzrelease=release)
    for cfile in cfiles:
        # 3a. Import file to docserver
        docserver.util.docserver_add_mp3(collection.collectionid, release.mbid, cfile.path, cfile.recordingid)
        cfile.set_state_finished()

    # 3b. Import release to dunya database
    abort = False
    if collection.do_import:
        try:
            directories = [os.path.join(collection.audio_directory, d.path) for d in release.collectiondirectory_set.all()]
            # Set the release logger releaseid to the id of the current release
            import_logger.releaseid = release.pk
            ri.import_release(release.mbid, directories)
        except Exception:
            abort = True
            tb = traceback.format_exc()
            release.add_log_message(tb)

    if not abort:
        # If the import succeeded, clean up.
        release.add_log_message("Release import finished")
        release.set_state_finished()
        # If the release was ignored and we've successfully imported it, unignore
        if original_ignore:
            release.ignore = False
            release.save()
    else:
        # 3c. If there was an error, set this release as "ignored"
        release.ignore = True
        release.save()
        release.add_log_message("Release import aborted due to failure")
        release.set_state_error()

def get_release_importer(collection):
    name = collection.name.lower()
    name = name.lower()
    data_coll = data.models.Collection.objects.get(collectionid=collection.collectionid)
    ri = None
    if "hindustani" in name:
        ri = hindustani_importer.HindustaniReleaseImporter(data_coll)
    elif "carnatic" in name:
        ri = carnatic_importer.CarnaticReleaseImporter(data_coll)
    elif "makam" in name:
        ri = makam_importer.MakamReleaseImporter(data_coll)
    elif "andalusian" in name:
        ri = andalusian_importer.AndalusianReleaseImporter(data_coll)
    elif "jingju" in name:
        ri = jingju_importer.JingjuReleaseImporter(data_coll)
    return ri

@app.task(base=CollectionDunyaTask)
def force_import_all_releases(collectionid):
    """ Reimport releases in a dashboard collection.
    This will (re)import all releases that are in the collection, even if they
    are marked as ignored or are finished, unless they are missing a directory
    """
    collection = models.Collection.objects.get(collectionid=collectionid)
    ri = get_release_importer(collection)
    if not ri:
        collection.add_log_message("Cannot discover importer based on collection name (does it include carnatic/hindustani/makam/andalusian?)")
        collection.set_state_error()
        return
    collection.set_state_importing()
    # unlike the non-force version, we select all releases, not
    # only ones that haven't been ignored
    releases = collection.musicbrainzrelease_set.all()
    unstarted = []
    for r in releases:
        matched_paths = r.collectiondirectory_set.all()
        if len(matched_paths):
            if r.ignore:
                r.ignore = False
                r.save()
            unstarted.append(r)
    for r in unstarted:
        import_release(r.id, ri)
    collection.set_state_finished()

def _get_musicbrainz_release_for_dir(dirname):
    """ Get a unique list of all the musicbrainz release IDs that
    are in tags in mp3 files in the given directory.
    """
    release_ids = set()
    for fname in _get_mp3_files(os.listdir(dirname)):
        fpath = os.path.join(dirname, fname)
        meta = compmusic.file_metadata(fpath)
        if meta:
            rel = meta["meta"]["releaseid"]
            if rel:
                release_ids.add(rel)
    return list(release_ids)

def _get_mp3_files(files):
    """ Take a list of files and return only the mp3 files """
    # TODO: This should be any audio file, replace with util method
    return [f for f in files if f.startswith("._") is False and ".mp3" in f]

def update_collection(collectionid):
    """ Sync the contents of a collection on musicbrainz.org to our local
        database.

        If new releases have been added, put them in our local database.
        If releases have been removed, remove referenes to them in our
        database.
    """
    found_releases = compmusic.get_releases_in_collection(collectionid)
    coll = models.Collection.objects.get(collectionid=collectionid)
    existing_releases = [str(r.mbid) for r in coll.musicbrainzrelease_set.all()]
    to_remove = set(existing_releases) - set(found_releases)
    to_add = set(found_releases) - set(existing_releases)

    for relid in to_add:
        try:
            mbrelease = compmusic.mb.get_release_by_id(relid, includes=["artists"])["release"]
            title = mbrelease["title"]
            artist = mbrelease["artist-credit-phrase"]
            rel, created = models.MusicbrainzRelease.objects.get_or_create(
                mbid=relid,
                collection=coll,
                defaults={"title": title, "artist": artist})
        except compmusic.mb.ResponseError:
            coll.add_log_message("The collection had an entry for %s but I can't find a release with that ID" % relid)

    for relid in to_remove:
        coll.musicbrainzrelease_set.filter(mbid=relid).delete()

def _match_directory_to_release(collectionid, root):
    """ Try and match a single directory containing audio files to a release
        that exists in the given collection.

        collectionid: the ID of the collection we want the directory to be in
        root: the root path of the directory containing audio files
    """
    coll = models.Collection.objects.get(collectionid=collectionid)
    collectionroot = coll.audio_directory
    # Remove the path to the collection from the path to the files (we only store relative paths)
    if not collectionroot.endswith("/"):
        collectionroot += "/"
    if root.startswith(collectionroot):
        # This should always be true since we scan from the collection root
        shortpath = root[len(collectionroot):]
    else:
        shortpath = root
    # Try and find the musicbrainz release for the files in the directory
    cd, created = models.CollectionDirectory.objects.get_or_create(collection=coll, path=shortpath)
    rels = _get_musicbrainz_release_for_dir(root)
    if len(rels) == 1:
        releaseid = rels[0]
        try:
            therelease = models.MusicbrainzRelease.objects.get(mbid=releaseid, collection=coll)
            cd.musicbrainzrelease = therelease
            cd.save()

            mp3files = _get_mp3_files(os.listdir(root))
            for f in mp3files:
                _create_collectionfile(cd, f)
        except models.MusicbrainzRelease.DoesNotExist:
            pass

def _create_collectionfile(cd, name):
    """arguments:
       cd: a collectiondirectory
       name: the name of the file, with no path information
    """
    path = os.path.join(cd.full_path, name)
    meta = compmusic.file_metadata(path)
    recordingid = meta["meta"].get("recordingid")
    size = os.path.getsize(path)
    cfile, created = models.CollectionFile.objects.get_or_create(name=name, directory=cd, recordingid=recordingid, defaults={'filesize': size})
    if not created:
        cfile.filesize = size
        cfile.save()

def scan_and_link(collectionid):
    """ Scan the root directory of a collection and see if any directories
    have been added or removed.

    If they've been removed, delete references to them. If they've been
    added, scan the files for a musicbrainz release id and match them
    to musicbrainz releases that are part of the collection.
    """

    coll = models.Collection.objects.get(collectionid=collectionid)
    collectionroot = coll.audio_directory
    if not collectionroot.endswith("/"):
        collectionroot += "/"
    found_directories = []
    for root, d, files in os.walk(collectionroot):
        mp3files = _get_mp3_files(files)
        if len(mp3files) > 0:
            found_directories.append(root)
    existing_directories = [c.full_path for c in coll.collectiondirectory_set.all()]

    to_remove = set(existing_directories) - set(found_directories)
    to_add = set(found_directories) - set(existing_directories)

    for d in to_add:
        _match_directory_to_release(collectionid, d)

    for d in to_remove:
        # We have a full path, but collectiondirectories are
        # stored with a partial path, so cut it.
        shortpath = d[len(collectionroot):]
        coll.collectiondirectory_set.get(path=shortpath).delete()

    # For every CollectionDirectory that isn't matched to a MusicbrainzRelease,
    # try and match it
    cds = coll.collectiondirectory_set.filter(musicbrainzrelease__isnull=True)
    for cd in cds:
        _match_directory_to_release(coll.collectionid, cd.full_path)

    _check_existing_directories(coll)

def _check_existing_directories(coll):
    # For all of the matched directories, look at the contents of them and
    # remove/add files as needed
    # We don't remove from the docserver because it's not very important to
    # remove them, and it's complex to cover all cases. We do this separately
    for cd in coll.collectiondirectory_set.all():
        files = os.listdir(cd.full_path)
        mp3files = _get_mp3_files(files)
        existing_f = cd.collectionfile_set.all()
        existing_names = [f.name for f in existing_f]

        to_remove = set(existing_names) - set(mp3files)
        to_add = set(mp3files) - set(existing_names)
        same_files = set(mp3files) & set(existing_names)

        for rm in to_remove:
            # If it was renamed, we will make a CollectionFile in
            # the to_add block below
            cd.collectionfile_set.get(name=rm).delete()

        for f in same_files:
            # Look through all files and see if the mbid has changed. If it's changed,
            # update the reference
            fileobject = cd.collectionfile_set.get(name=f)
            meta = compmusic.file_metadata(os.path.join(cd.full_path, f))
            recordingid = meta["meta"].get("recordingid")
            if recordingid != fileobject.recordingid:
                fileobject.recordingid = recordingid
                fileobject.save()

        if to_add:
            # If there are new files in the directory, just run _match again and
            # it will create the new file objects
            _match_directory_to_release(coll.collectionid, cd.full_path)

@app.task
def delete_collection(collectionpk):
    collection = models.Collection.objects.get(pk=collectionpk)
    collection.delete()
<|endoftext|>


<commit_before>## This file is part of Indico.
## Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
##
## Indico is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.
##
## Indico 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
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Indico; if not, see <http://www.gnu.org/licenses/>.

import pytest

pytest_plugins = 'indico.modules.rb.testing.fixtures'

def test_owner(dummy_room, dummy_user):
    assert dummy_room.owner.id == dummy_user.id
    dummy_room.owner_id = 'xxx'
    assert dummy_room.owner is None

@pytest.mark.parametrize(('room_args', 'expected_name'), (
    ({}, u'1-2-3'),
    ({'building': u'X'}, u'X-2-3'),
    ({'name': u'Test'}, u'1-2-3 - Test'),
    ({'name': u'm\xf6p'}, u'1-2-3 - m\xf6p')
))
def test_full_name(create_room, room_args, expected_name):
    room = create_room(**room_args)
    assert room.full_name == expected_name
<commit_msg>wip room tests<commit_after>## This file is part of Indico.
## Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
##
## Indico is free software; you can redistribute it and/or
## modify it under the terms of the GNU General Public License as
## published by the Free Software Foundation; either version 3 of the
## License, or (at your option) any later version.
##
## Indico 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
## General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with Indico; if not, see <http://www.gnu.org/licenses/>.

import pytest

from indico.modules.rb.models.rooms import Room

pytest_plugins = 'indico.modules.rb.testing.fixtures'

def test_is_auto_confirm(create_room, bool_flag):
    room = create_room(reservations_need_confirmation=bool_flag)
    assert room.is_auto_confirm != bool_flag
    assert Room.find_first(is_auto_confirm=bool_flag) is None
    assert Room.find_first(is_auto_confirm=not bool_flag) == room

@pytest.mark.parametrize(('value', 'expected'), (
    ('xxx', True),
    ('', False)
))
def test_has_booking_groups(create_room_attribute, dummy_room, value, expected):
    create_room_attribute('allowed-booking-group')
    assert not dummy_room.has_booking_groups
    dummy_room.set_attribute_value('allowed-booking-group', value)
    assert dummy_room.has_booking_groups == expected

def test_has_projector(create_equipment_type, dummy_room, db):
    assert not dummy_room.has_projector
    dummy_room.available_equipment.append(create_equipment_type('Computer Projector'))
    db.session.flush()
    assert dummy_room.has_projector

def test_has_webcast_recording(create_equipment_type, dummy_room, db):
    assert not dummy_room.has_webcast_recording
    dummy_room.available_equipment.append(create_equipment_type('Webcast/Recording'))
    db.session.flush()
    assert dummy_room.has_webcast_recording

@pytest.mark.parametrize(('reservable', 'booking_group', 'expected'), (
    (True, '', True),
    (True, 'xxx', False),
    (False, '', False),
    (False, 'xxx', False)
))
def test_is_public(create_room_attribute, dummy_room, reservable, booking_group, expected):
    create_room_attribute('allowed-booking-group')
    dummy_room.set_attribute_value('allowed-booking-group', booking_group)
    dummy_room.is_reservable = reservable
    assert dummy_room.is_public == expected

def test_owner(dummy_room, dummy_user):
    assert dummy_room.owner.id == dummy_user.id
    dummy_room.owner_id = 'xxx'
    assert dummy_room.owner is None

@pytest.mark.parametrize(('room_args', 'expected_name'), (
    ({}, u'1-2-3'),
    ({'building': u'X'}, u'X-2-3'),
    ({'name': u'Test'}, u'1-2-3 - Test'),
    ({'name': u'm\xf6p'}, u'1-2-3 - m\xf6p')
))
def test_full_name(create_room, room_args, expected_name):
    room = create_room(**room_args)
    assert room.full_name == expected_name
<|endoftext|>


<commit_before>from __future__ import absolute_import
import matplotlib.pyplot as plt
import seaborn.apionly as sns
import scipy.sparse as sp
import numpy as np
import scipy.io
import re
import timeit
import comdet.biclustering as bc
import comdet.test.utils as test_utils
import comdet.pme.line as line
import comdet.pme.circle as circle
import comdet.pme.sampling as sampling
import comdet.pme.acontrario.line as ac_line
import comdet.pme.acontrario.circle as ac_circle

def base_plot(x):
    x_lim = (x[:, 0].min() - 0.1, x[:, 0].max() + 0.1)
    y_lim = (x[:, 1].min() - 0.1, x[:, 1].max() + 0.1)
    delta_x = x_lim[1] - x_lim[0]
    delta_y = y_lim[1] - y_lim[0]
    min_delta = min([delta_x, delta_y])
    delta_x /= min_delta
    delta_y /= min_delta
    fig_size = (4 * delta_x, 4 * delta_y)

    plt.figure(figsize=fig_size)
    plt.xlim(x_lim)
    plt.ylim(y_lim)
    plt.scatter(x[:, 0], x[:, 1], c='w', marker='o', s=10)

def plot_models(models, palette=None, **kwargs):
    if palette is not None and 'color' in kwargs:
        raise RuntimeError('Cannot specify palette and color simultaneously.')
    for i, mod in enumerate(models):
        if palette is not None:
            kwargs['color'] = palette[i]
        mod.plot(**kwargs)

def plot_final_models(x, models, palette):
    base_plot(x)
    plot_models(models, palette=palette, linewidth=5, alpha=0.5)

def plot_original_models(x, original_models, right_factors, palette):
    base_plot(x)
    for i, rf in enumerate(right_factors):
        plot_models([original_models[j] for j in sp.find(rf)[1]],
                    color=palette[i], alpha=0.5)

def ground_truth(n_elements, n_groups=5, group_size=50):
    gt_groups = []
    for i in range(n_groups):
        v = np.zeros((n_elements,), dtype=bool)
        v[i * group_size:(i+1) * group_size] = True
        gt_groups.append(v)
    return gt_groups

def run_biclustering(model_class, x, original_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix, palette='Set1'):
    t = timeit.default_timer()
    bic_list = bc.bicluster(deflator)
    t1 = timeit.default_timer() - t
    print 'Time:', t1

    mod_inliers_list, bic_list = test_utils.clean(model_class, x, ac_tester,
                                                  bic_list)

    palette = sns.color_palette(palette, len(bic_list))

    plt.figure()
    bc.preference.plot(pref_matrix, bic_list=bic_list, palette=palette)
    plt.savefig(output_prefix + '_pref_mat.pdf', dpi=600)

    plot_final_models(x, [mi[0] for mi in mod_inliers_list], palette=palette)
    plt.savefig(output_prefix + '_final_models.pdf', dpi=600)

    plot_original_models(x, original_models, [bic[1] for bic in bic_list],
                         palette)
    plt.savefig(output_prefix + '_bundles.pdf', dpi=600)

    test_utils.compute_measures(gt_groups, [bic[0] for bic in bic_list])

def test(model_class, x, name, ransac_gen, ac_tester, gt_groups):
    print name, x.shape

    output_prefix = '../results/' + name

    base_plot(x)
    plt.savefig(output_prefix + '_data.pdf', dpi=600)

    pref_matrix, orig_models = test_utils.build_preference_matrix(x.shape[0],
                                                                  ransac_gen,
                                                                  ac_tester)
    print 'Preference matrix size:', pref_matrix.shape

    base_plot(x)
    plot_models(orig_models, alpha=0.2)
    plt.savefig(output_prefix + '_original_models.pdf', dpi=600)

    plt.figure()
    bc.preference.plot(pref_matrix)
    plt.savefig(output_prefix + '_pref_mat.pdf', dpi=600)

    print 'Running regular bi-clustering'
    deflator = bc.deflation.Deflator(pref_matrix)
    run_biclustering(model_class, x, orig_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix + '_bic_reg')

    print 'Running compressed bi-clustering'
    compression_level = 128
    deflator = bc.deflation.L1CompressedDeflator(pref_matrix, compression_level)
    run_biclustering(model_class, x, orig_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix + '_bic_comp')

if __name__ == '__main__':
    sampling_factor = 20
    inliers_threshold = 0.03
    epsilon = 0

    configuration = {'Star': (line.Line, sampling.UniformSampler(),
                              ac_line.LocalNFA),
                     'Stairs': (line.Line, sampling.UniformSampler(),
                                ac_line.LocalNFA),
                     'Circles': (circle.Circle, sampling.UniformSampler(),
                                 ac_circle.LocalNFA),
                     }

    # configuration = {'Star': (line.Line, sampling.SimpleSampler(),
    #                           ac_line.LocalNFA),
    #                  'Stairs': (line.Line,
    #                             sampling.GaussianLocalSampler(0.05),
    #                             ac_line.LocalNFA),
    #                  'Circles': (circle.Circle,
    #                              sampling.GaussianLocalSampler(0.5),
    #                              ac_circle.LocalNFA),
    #                  }
    #
    # configuration = {'Star': (line.Line, sampling.SimpleSampler(),
    #                           ac_line.GlobalNFA),
    #                  'Stairs': (line.Line,
    #                             sampling.GaussianLocalSampler(0.05),
    #                             ac_line.GlobalNFA),
    #                  'Circles': (circle.Circle,
    #                              sampling.GaussianLocalSampler(0.5),
    #                              ac_circle.GlobalNFA),
    #                  }

    examples = scipy.io.loadmat('../data/JLinkageExamples.mat')
    for current_example in examples:
        exp_type = None
        for c in configuration:
            if current_example.find(c) == 0:
                exp_type = c
                break
        else:
            if exp_type is None:
                continue

        model_class, sampler, ac_tester_class = configuration[exp_type]
        data = examples[current_example].T

        sampler.n_samples = data.shape[0] * sampling_factor
        ransac_gen = sampling.ransac_generator(model_class, data, sampler,
                                               inliers_threshold)
        ac_tester = ac_tester_class(data, epsilon, inliers_threshold)

        match = re.match(exp_type + '[0-9]*_', current_example)
        try:
            match = re.match('[0-9]+', match.group()[len(exp_type):])
            n_groups = int(match.group())
        except AttributeError:
            n_groups = 4
            continue
        gt_groups = ground_truth(data.shape[0], n_groups=n_groups,
                                 group_size=50)

        print '-'*40
        seed = 0
        # seed = np.random.randint(0, np.iinfo(np.uint32).max)
        print 'seed:', seed
        np.random.seed(seed)

        test(model_class, data, current_example, ransac_gen, ac_tester,
             gt_groups)

    # plt.show()<commit_msg>Example 2d: refactoring and console redirection<commit_after>from __future__ import absolute_import
import sys
import matplotlib.pyplot as plt
import seaborn.apionly as sns
import scipy.sparse as sp
import numpy as np
import scipy.io
import re
import timeit
import comdet.biclustering as bc
import comdet.test.utils as test_utils
import comdet.pme.preference as pref
import comdet.pme.sampling as sampling
import comdet.pme.line as line
import comdet.pme.circle as circle
import comdet.pme.acontrario.line as ac_line
import comdet.pme.acontrario.circle as ac_circle

def base_plot(x):
    x_lim = (x[:, 0].min() - 0.1, x[:, 0].max() + 0.1)
    y_lim = (x[:, 1].min() - 0.1, x[:, 1].max() + 0.1)
    delta_x = x_lim[1] - x_lim[0]
    delta_y = y_lim[1] - y_lim[0]
    min_delta = min([delta_x, delta_y])
    delta_x /= min_delta
    delta_y /= min_delta
    fig_size = (4 * delta_x, 4 * delta_y)

    plt.figure(figsize=fig_size)
    plt.xlim(x_lim)
    plt.ylim(y_lim)
    plt.scatter(x[:, 0], x[:, 1], c='w', marker='o', s=10)

def plot_models(models, palette=None, **kwargs):
    if palette is not None and 'color' in kwargs:
        raise RuntimeError('Cannot specify palette and color simultaneously.')
    for i, mod in enumerate(models):
        if palette is not None:
            kwargs['color'] = palette[i]
        mod.plot(**kwargs)

def plot_final_models(x, models, palette):
    base_plot(x)
    plot_models(models, palette=palette, linewidth=5, alpha=0.5)

def plot_original_models(x, original_models, right_factors, palette):
    base_plot(x)
    for i, rf in enumerate(right_factors):
        plot_models([original_models[j] for j in sp.find(rf)[1]],
                    color=palette[i], alpha=0.5)

def ground_truth(n_elements, n_groups=5, group_size=50):
    gt_groups = []
    for i in range(n_groups):
        v = np.zeros((n_elements,), dtype=bool)
        v[i * group_size:(i+1) * group_size] = True
        gt_groups.append(v)
    return gt_groups

def run_biclustering(model_class, x, original_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix, palette='Set1'):
    t = timeit.default_timer()
    bic_list = bc.bicluster(deflator)
    t1 = timeit.default_timer() - t
    print 'Time:', t1

    mod_inliers_list, bic_list = test_utils.clean(model_class, x, ac_tester,
                                                  bic_list)

    palette = sns.color_palette(palette, len(bic_list))

    plt.figure()
    pref.plot(pref_matrix, bic_list=bic_list, palette=palette)
    plt.savefig(output_prefix + '_pref_mat.pdf', dpi=600)

    plot_final_models(x, [mi[0] for mi in mod_inliers_list], palette=palette)
    plt.savefig(output_prefix + '_final_models.pdf', dpi=600)

    plot_original_models(x, original_models, [bic[1] for bic in bic_list],
                         palette)
    plt.savefig(output_prefix + '_bundles.pdf', dpi=600)

    test_utils.compute_measures(gt_groups, [bic[0] for bic in bic_list])

def test(model_class, x, name, ransac_gen, ac_tester, gt_groups):
    print name, x.shape

    output_prefix = '../results/' + name

    base_plot(x)
    plt.savefig(output_prefix + '_data.pdf', dpi=600)

    pref_matrix, orig_models = pref.build_preference_matrix(x.shape[0],
                                                            ransac_gen,
                                                            ac_tester)
    print 'Preference matrix size:', pref_matrix.shape

    base_plot(x)
    plot_models(orig_models, alpha=0.2)
    plt.savefig(output_prefix + '_original_models.pdf', dpi=600)

    plt.figure()
    pref.plot(pref_matrix)
    plt.savefig(output_prefix + '_pref_mat.pdf', dpi=600)

    print 'Running regular bi-clustering'
    deflator = bc.deflation.Deflator(pref_matrix)
    run_biclustering(model_class, x, orig_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix + '_bic_reg')

    print 'Running compressed bi-clustering'
    compression_level = 128
    deflator = bc.deflation.L1CompressedDeflator(pref_matrix, compression_level)
    run_biclustering(model_class, x, orig_models, pref_matrix, deflator,
                     ac_tester, gt_groups, output_prefix + '_bic_comp')

if __name__ == '__main__':
    sys.stdout = test_utils.Logger("test_2d.txt")

    sampling_factor = 20
    inliers_threshold = 0.03
    epsilon = 0

    configuration = {'Star': (line.Line, sampling.UniformSampler(),
                              ac_line.LocalNFA),
                     'Stairs': (line.Line, sampling.UniformSampler(),
                                ac_line.LocalNFA),
                     'Circles': (circle.Circle, sampling.UniformSampler(),
                                 ac_circle.LocalNFA),
                     }

    examples = scipy.io.loadmat('../data/JLinkageExamples.mat')
    for current_example in examples:
        exp_type = None
        for c in configuration:
            if current_example.find(c) == 0:
                exp_type = c
                break
        else:
            if exp_type is None:
                continue

        model_class, sampler, ac_tester_class = configuration[exp_type]
        data = examples[current_example].T

        def inliers(model):
            return sampling.inliers(model, data, inliers_threshold)

        sampler.n_samples = data.shape[0] * sampling_factor
        mg = sampling.ModelGenerator(model_class, data)
        ransac_gen = sampling.RansacGenerator(sampler, mg, inliers)
        ac_tester = ac_tester_class(data, epsilon, inliers_threshold)

        match = re.match(exp_type + '[0-9]*_', current_example)
        try:
            match = re.match('[0-9]+', match.group()[len(exp_type):])
            n_groups = int(match.group())
        except AttributeError:
            n_groups = 4
            continue
        gt_groups = ground_truth(data.shape[0], n_groups=n_groups,
                                 group_size=50)

        print '-'*40
        seed = 0
        # seed = np.random.randint(0, np.iinfo(np.uint32).max)
        print 'seed:', seed
        np.random.seed(seed)

        test(model_class, data, current_example, ransac_gen, ac_tester,
             gt_groups)

    # plt.show()<|endoftext|>


<commit_before># Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.appengine.api import memcache
from google.appengine.api import urlfetch
import webapp2

import base64

BASE = 'https://android.googlesource.com/platform/external/perfetto.git/' \
       '+/master/%s?format=TEXT'

RESOURCES = {
    'traceconv': 'tools/traceconv',
    'trace_processor': 'tools/trace_processor',
}

class RedirectHandler(webapp2.RequestHandler):

  def get(self):
    self.error(301)
    self.response.headers['Location'] = 'https://www.perfetto.dev/'

class GitilesMirrorHandler(webapp2.RequestHandler):

  def get(self, resource):
    resource = resource.lower()
    if resource not in RESOURCES:
      self.error(404)
      self.response.out.write('Rerource "%s" not found' % resource)
      return

    url = BASE % RESOURCES[resource]
    contents = memcache.get(url)
    if not contents or self.request.get('reload'):
      result = urlfetch.fetch(url)
      if result.status_code != 200:
        memcache.delete(url)
        self.response.set_status(result.status_code)
        self.response.write(
            'http error %d while fetching %s' % (result.status_code, url))
        return
      contents = base64.b64decode(result.content)
      memcache.set(url, contents, time=3600)  # 1h
    self.response.headers['Content-Type'] = 'text/plain'
    self.response.headers['Content-Disposition'] = \
        'attachment; filename="%s"' % resource
    self.response.write(contents)

app = webapp2.WSGIApplication([
    ('/', RedirectHandler),
    ('/(.*)', GitilesMirrorHandler),
],
                              debug=True)
<commit_msg>fix typo in get.perfetto.dev am: d03fd291f3 am: 86c5037948 am: c6e9282977 am: 2c744e5e18 am: a3dc76eb29<commit_after># Copyright (C) 2019 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.appengine.api import memcache
from google.appengine.api import urlfetch
import webapp2

import base64

BASE = 'https://android.googlesource.com/platform/external/perfetto.git/' \
       '+/master/%s?format=TEXT'

RESOURCES = {
    'traceconv': 'tools/traceconv',
    'trace_processor': 'tools/trace_processor',
}

class RedirectHandler(webapp2.RequestHandler):

  def get(self):
    self.error(301)
    self.response.headers['Location'] = 'https://www.perfetto.dev/'

class GitilesMirrorHandler(webapp2.RequestHandler):

  def get(self, resource):
    resource = resource.lower()
    if resource not in RESOURCES:
      self.error(404)
      self.response.out.write('Resource "%s" not found' % resource)
      return

    url = BASE % RESOURCES[resource]
    contents = memcache.get(url)
    if not contents or self.request.get('reload'):
      result = urlfetch.fetch(url)
      if result.status_code != 200:
        memcache.delete(url)
        self.response.set_status(result.status_code)
        self.response.write(
            'http error %d while fetching %s' % (result.status_code, url))
        return
      contents = base64.b64decode(result.content)
      memcache.set(url, contents, time=3600)  # 1h
    self.response.headers['Content-Type'] = 'text/plain'
    self.response.headers['Content-Disposition'] = \
        'attachment; filename="%s"' % resource
    self.response.write(contents)

app = webapp2.WSGIApplication([
    ('/', RedirectHandler),
    ('/(.*)', GitilesMirrorHandler),
],
                              debug=True)
<|endoftext|>


<commit_before>import datetime as dt
import logging

from comics.common.models import Comic
from comics.crawler.utils import get_comic_module

logger = logging.getLogger('comics.crawler.meta')

class ComicMetaLoader(object):
    def __init__(self, options):
        self.comic_slugs = self._get_comic_slugs(options)

    def start(self):
        for comic_slug in self.comic_slugs:
            logger.info('Loading meta data for %s', comic_slug)
            self._try_load_comic_meta(comic_slug)

    def stop(self):
        pass

    def _get_comic_slugs(self, options):
        comic_slugs = options.get('comic_slugs', None)
        if comic_slugs is None or len(comic_slugs) == 0:
            logger.error('Loading all comics is not implemented yet, '
                'use "-c COMIC".')
            return [] # TODO Add all comic slugs
        else:
            return comic_slugs

    def _try_load_comic_meta(self, comic_slug):
        try:
            self._load_comic_meta(comic_slug)
        except Exception, error:
            logger.exception(error)

    def _load_comic_meta(self, comic_slug):
        logger.debug('Importing comic module')
        comic_module = get_comic_module(comic_slug)
        comic_meta = comic_module.ComicMeta()
        logger.debug('Loading comic into database')
        comic_meta.create_comic()

class BaseComicMeta(object):
    # Required values
    name = None
    language = None
    url = None

    # Default values
    start_date = None
    end_date = None
    history_capable_date = None
    history_capable_days = None
    has_reruns = False
    schedule = None
    time_zone = None
    rights = None

    @property
    def slug(self):
        return self.__module__.split('.')[-1]

    def create_comic(self):
        comic = Comic(
            name=self.name,
            slug=self.slug,
            language=self.language,
            url=self.url)
        if self.start_date:
            comic.start_date = self._get_date(self.start_date)
        if self.end_date:
            comic.end_date = self._get_date(self.end_date)
        if self.history_capable_date:
            comic.history_capable_date = self._get_date(
                self.history_capable_date)
        if self.history_capable_days:
            comic.history_capable_days = self.history_capable_days
        if self.has_reruns:
            comic.has_reruns = self.has_reruns
        if self.schedule:
            comic.schedule = self.schedule
        if self.time_zone:
            comic.time_zone = self.time_zone
        if self.rights:
            comic.rights = self.rights
        comic.save()

    def _get_date(self, date):
        return dt.datetime.strptime(date, '%Y-%m-%d').date()
<commit_msg>Support overwriting existing comics in the database<commit_after>import datetime as dt
import logging

from comics.common.models import Comic
from comics.crawler.utils import get_comic_module

logger = logging.getLogger('comics.crawler.meta')

class ComicMetaLoader(object):
    def __init__(self, options):
        self.comic_slugs = self._get_comic_slugs(options)

    def start(self):
        for comic_slug in self.comic_slugs:
            logger.info('Loading meta data for %s', comic_slug)
            self._try_load_comic_meta(comic_slug)

    def stop(self):
        pass

    def _get_comic_slugs(self, options):
        comic_slugs = options.get('comic_slugs', None)
        if comic_slugs is None or len(comic_slugs) == 0:
            logger.error('Loading all comics is not implemented yet, '
                'use "-c COMIC".')
            return [] # TODO Add all comic slugs
        else:
            return comic_slugs

    def _try_load_comic_meta(self, comic_slug):
        try:
            self._load_comic_meta(comic_slug)
        except Exception, error:
            logger.exception(error)

    def _load_comic_meta(self, comic_slug):
        logger.debug('Importing comic module')
        comic_module = get_comic_module(comic_slug)
        comic_meta = comic_module.ComicMeta()
        logger.debug('Loading comic into database')
        comic_meta.create_comic()

class BaseComicMeta(object):
    # Required values
    name = None
    language = None
    url = None

    # Default values
    start_date = None
    end_date = None
    history_capable_date = None
    history_capable_days = None
    has_reruns = False
    schedule = None
    time_zone = None
    rights = None

    @property
    def slug(self):
        return self.__module__.split('.')[-1]

    def create_comic(self):
        if Comic.objects.filter(slug=self.slug).count():
            comic = Comic.objects.get(slug=self.slug)
            comic.name = self.name
            comic.language = self.language
            comic.url = self.url
        else:
            comic = Comic(
                name=self.name,
                slug=self.slug,
                language=self.language,
                url=self.url)
        if self.start_date:
            comic.start_date = self._get_date(self.start_date)
        if self.end_date:
            comic.end_date = self._get_date(self.end_date)
        if self.history_capable_date:
            comic.history_capable_date = self._get_date(
                self.history_capable_date)
        if self.history_capable_days:
            comic.history_capable_days = self.history_capable_days
        if self.has_reruns:
            comic.has_reruns = self.has_reruns
        if self.schedule:
            comic.schedule = self.schedule
        if self.time_zone:
            comic.time_zone = self.time_zone
        if self.rights:
            comic.rights = self.rights
        comic.save()

    def _get_date(self, date):
        return dt.datetime.strptime(date, '%Y-%m-%d').date()
<|endoftext|>