def list_admin() -> None:
def toggle_admin(email, state) -> None: def add_batch(batch_id: str,
              count: int,
              task_type: AbstractTaskType,
              default_batch: str,
              batch_meta: Optional[Dict] = None) -> None:
def validate_batch(ctx,
                   param: str,
                   value: str,
                   default_batch: str) -> str:
def batches_list() -> List[str]: def open_anything(filename: str):
def load_tasks(task_type: AbstractTaskType, path: str, batch: str) -> int:
def _load_tasks_file(task_type: AbstractTaskType, path: str, batch: str) -> int:
    def _safe_load(fl):
def export_reports(task_id: AbstractTaskType, path: str, batch: str, closed: bool) -> None: class AbstractTaskType:
    """
    The main entity in the application. Contains all the logic we need to
    handle task emission/accounting.

    Could be overridden in plugins to fit your needs.

    The simplest and most common scenario of being overridden is to have own
     task type name and description to separate your tasks from any other.
    """
    # models
    answer_model = None
    task_model = None

    template = ''
    helptext_template = ''
    type_name = ''

    redundancy = 3
    JS_ASSETS = []
    CSS_ASSETS = []

    # properties
    _name = ''
    _description = ''

    # Meta information on project to copy to task batches
    _task_type_meta = {}

    # managers
    _work_session_manager = None  # type: WorkSessionManager
    _leaderboard_manager = None  # type: LeaderBoardManager

    def __init__(self, settings: Dict[str, Any]) -> None:
        """
        Constructor.

        :param settings: We pass global settings dictionary into the
        constructor when instantiating plugins. Could be useful for plugins.
        :type settings: Dict[str, Any]
        """
        self._logger = logging.getLogger('vulyk.app')

        self._leaderboard_manager = \
            self._leaderboard_manager or LeaderBoardManager(self.type_name,
                                                            self.answer_model,
                                                            User)
        self._work_session_manager = \
            self._work_session_manager or WorkSessionManager(WorkSession)

        assert issubclass(self.task_model, AbstractTask), \
            'You should define task_model property'
        assert issubclass(self.answer_model, AbstractAnswer), \
            'You should define answer_model property'

        assert isinstance(self._work_session_manager, WorkSessionManager), \
            'You should define _work_session_manager property'
        assert isinstance(self._leaderboard_manager, LeaderBoardManager), \
            'You should define _leaderboard_manager property'

        assert self.type_name, 'You should define type_name (underscore)'
        assert self.template, 'You should define template'
        assert isinstance(self._task_type_meta, dict), \
            'Batch meta must of dict type'

    @property
    def name(self) -> str:
        """
        Human-readable name of the plugin.

        :return: Name of the task type.
        :rtype: str
        """
        return self._name if len(self._name) > 0 else self.type_name

    @property
    def task_type_meta(self) -> Dict[str, Any]:
        """
        Dict with task type metadata (freeform dict)

        :return: project specific metadata
        :rtype: Dict[str, Any]
        """
        return self._task_type_meta

    @property
    def description(self) -> str:
        """
        Explicit description of the plugin.

        :return: Plugin description.
        :rtype: str
        """
        return self._description if len(self._description) > 0 else ''

    @property
    def work_session_manager(self) -> WorkSessionManager:
        """
        Returns current instance of WorkSessionManager used in the task type.

        :return: Active WorkSessionManager instance.
        :rtype: WorkSessionManager
        """
        return self._work_session_manager

    def import_tasks(
        self,
        tasks: List[Dict],
        batch: Optional[AnyStr]
    ) -> None:
        """Imports tasks from an iterable over dicts
        io is left out of scope here.

        :param tasks: An iterable over dicts
        :type tasks: List[Dict]
        :param batch: Batch ID (optional)
        :type batch: Optional[AnyStr]

        :raise: TaskImportError
        """
        errors = (AttributeError, TypeError, ValidationError, OperationError,
                  AssertionError)
        bulk = []

        try:
            for task in tasks:
                assert isinstance(task, dict)

                bulk.append(self.task_model(
                    id=sha1(json.dumps(task).encode('utf8')).hexdigest()[:20],
                    batch=batch,
                    task_type=self.type_name,
                    task_data=task))

            self.task_model.objects.insert(bulk)

            self._logger.debug('Inserted %s tasks in batch %s for plugin <%s>',
                               len(bulk), batch, self.name)
        except errors as e:
            raise TaskImportError('Can\'t load task: {}'.format(e))

    def export_reports(
        self,
        batch: str,
        closed: bool = True,
        qs=None
    ) -> Generator[Dict[str, Any], None, None]:
        """Exports results. IO is left out of scope here as well

        :param batch: Certain batch to extract
        :type batch: str
        :param closed: Specify if we need to export only closed tasks reports
        :type closed: bool
        :param qs: Queryset, an optional argument. Default value is QS that
                   exports all tasks with amount of answers > redundancy
        :type qs: QuerySet

        :returns: Generator of lists of dicts with results
        :rtype: Generator[Dict[str, Any], None, None]
        """
        if qs is None:
            query = Q()

            if batch != '__all__':
                query &= Q(batch=batch)

            if closed:
                query &= Q(closed=closed)

            qs = self.task_model.objects(query)

        for task in qs:
            yield list(map(lambda a: a.as_dict(),
                           self.answer_model.objects(task=task)))

    def get_leaders(self) -> List[Tuple[ObjectId, int]]:
        """Return sorted list of tuples (user_id, tasks_done)

        :returns: list of tuples (user_id, tasks_done)
        :rtype: List[Tuple[ObjectId, int]]
        """
        return self._leaderboard_manager.get_leaders()

    def get_leaderboard(self, limit: int = 10) -> List[Dict]:
        """Find users who contributed the most

        :param limit: number of top users to return
        :type limit: int

        :returns: List of dicts {user: user_obj, freq: count}
        :rtype: List[Dict]
        """
        return self._leaderboard_manager.get_leaderboard(limit)

    def get_next(self, user: User) -> Dict:
        """
        Finds given user a new task and starts new WorkSession

        :param user: an instance of User model
        :type user: User

        :returns: Prepared dictionary of model, or empty dictionary
        :rtype: Dict
        """
        task = self._get_next_task(user)

        if task is not None:
            # Not sure if we should do that here on GET requests
            self._work_session_manager.start_work_session(task, user.id)

            self._logger.debug('Assigned task %s to user %s', task.id, user.id)

            return task.as_dict()
        else:
            self._logger.debug('No suitable task found for  user %s', user.id)

            return {}

    def _get_next_task(self, user: User) -> Optional[AbstractTask]:
        """
        Finds given user a new task

        :param user: an instance of User model
        :type user: User

        :returns: Model instance or None
        :rtype: Optional[AbstractTask]
        """
        rs = None
        base_q = Q(task_type=self.type_name) \
            & Q(users_processed__nin=[user]) \
            & Q(closed__ne=True)

        for batch in Batch \
                .objects(task_type=self.type_name, closed__ne=True) \
                .order_by('id'):

            if batch.tasks_count == batch.tasks_processed:
                continue

            rs = self.task_model.objects(
                base_q & Q(users_skipped__nin=[user]) & Q(batch=batch.id))

            if rs.count() == 0:
                del rs
                rs = self.task_model.objects(base_q & Q(batch=batch.id))

            if rs.count() > 0:
                break
        else:
            # Now searching w/o batch restriction
            rs = self.task_model.objects(
                base_q & Q(users_skipped__nin=[user]))

            if rs.count() == 0:
                del rs
                rs = self.task_model.objects(base_q)

        if rs:
            _id = random.choice(rs.distinct('id') or [])

            try:
                return rs.get(id=_id)
            except self.task_model.DoesNotExist:
                self._logger.error(
                    'DoesNotExist when trying to fetch task {}'.format(_id))

                return None
        else:
            return None

    def record_activity(
        self,
        user_id: Union[AnyStr, ObjectId],
        task_id: AnyStr,
        seconds: int
    ) -> None:
        """
        Increases the counter of activity for current user in given task.

        :param user_id: ID of user, who gets new task
        :type user_id: Union[AnyStr, ObjectId]
        :param task_id: Current task
        :type task_id: AnyStr
        :param seconds: User was active for
        :type seconds: int

        :raises: TaskSkipError, TaskNotFoundError
        """
        try:
            task = self.task_model.objects.get(id=task_id,
                                               task_type=self.type_name)

            self._work_session_manager.record_activity(task, user_id, seconds)

            self._logger.debug('Recording %s seconds of activity of user %s '
                               'on task %s', seconds, user_id, task_id)
        except self.task_model.DoesNotExist:
            raise TaskNotFoundError()

    def skip_task(self, task_id: AnyStr, user: User):
        """
        Marks given task as a skipped by a given user
        Assumes that user is eligible for this kind of tasks

        :param task_id: Given task ID
        :type task_id: AnyStr
        :param user: an instance of User model who provided an answer
        :type user: User

        :raises: TaskSkipError, TaskNotFoundError
        """
        try:
            task = self.task_model.objects.get(
                id=task_id,
                task_type=self.type_name)

            task.update(add_to_set__users_skipped=user)
            self._work_session_manager.delete_work_session(task, user.id)

            self._logger.debug('User %s skipped the task %s', user.id, task_id)
        except self.task_model.DoesNotExist:
            raise TaskNotFoundError()
        except OperationError as err:
            raise TaskSkipError('Can not skip the task: {0}.'.format(err))

    def on_task_done(
        self,
        user: User,
        task_id: AnyStr,
        result: Dict[str, Any]
    ) -> None:
        """
        Saves user's answers for a given task.
        Assumes that user is eligible for this kind of tasks.

        :param task_id: Given task ID
        :type task_id: AnyStr
        :param user: an instance of User model who provided an answer
        :type user: User
        :param result: Task solving result
        :type result: Dict[str, Any]

        :raises: TaskSaveError - in case of general problems
        :raises: TaskValidationError - in case of validation problems
        """

        answer = None
        try:
            task = self.task_model.objects.get(
                id=task_id,
                task_type=self.type_name)
        except self.task_model.DoesNotExist:
            raise TaskNotFoundError('Task with ID {id} not found while '
                                    'trying to save an answer from {user!r}.'
                                    .format(id=task_id, user=user))

        try:
            answer = self.answer_model.objects.create(
                task=task,
                created_by=user,
                created_at=datetime.now(),
                task_type=self.type_name,
                result=result)

            # update task
            closed = self._update_task_on_answer(task, answer, user)
            # update user
            user.update(inc__processed=1)
            # update stats record
            self._work_session_manager.end_work_session(task, user.id, answer)

            self._logger.debug('User %s has done task %s', user.id, task_id)

            if closed and task.batch is not None:
                Batch.task_done_in(batch_id=task.batch.id)
        except NotUniqueError:
            raise TaskValidationError('Attempt to save over the existing '
                                      'answer for task {id} by user {user!r}'
                                      .format(id=task_id, user=user))
        except ValidationError as err:
            raise TaskValidationError(err, get_tb())
        except (OperationError, LookUpError, InvalidQueryError) as err:
            raise TaskSaveError(err, get_tb())

    def _is_ready_for_autoclose(
        self,
        task: AbstractTask,
        answer: AbstractAnswer
    ) -> bool:
        """
        Checks if task could be closed before it
        Should be overridden if you need more complex logic.

        :param task: an instance of self.task_model model
        :type task: AbstractTask
        :param answer: Task solving result
        :type answer: AbstractAnswer

        :returns: How many identical answers we got
        :rtype: bool
        """
        return False

    def _update_task_on_answer(
        self,
        task: AbstractTask,
        answer: AbstractAnswer,
        user: User
    ) -> bool:
        """
        Sets flag 'closed' to True if task's goal has been reached

        :param task: an instance of self.task_model model
        :type task: AbstractTask
        :param answer: Task solving result
        :type answer: AbstractAnswer
        :param user: an instance of User model who provided an answer
        :type user: User

        :rtype: bool
        """
        users_count = task.users_count + 1
        update_q = {
            'inc__users_count': 1,
            'add_to_set__users_processed': user}

        closed = self._is_ready_for_autoclose(task, answer) \
            or (users_count >= self.redundancy)

        if closed:
            update_q['set__closed'] = closed

        num_changed = self.task_model \
            .objects(id=task.id, closed=False) \
            .update_one(**update_q)  # type: int

        if closed and num_changed == 0:
            update_q.pop('set__closed', None)
            closed = False

            task.update(**update_q)

        return closed

    def to_dict(self) -> Dict[str, Any]:
        """
        Prepare simplified dict that contains basic info about the task type.

        :return: distilled dict with basic info
        :rtype: Dict[str, Any]
        """

        closed_tasks = self.task_model \
            .objects(closed=True) \
            .count()  # type: int
        tasks = self.task_model.objects().count()  # type: int
        open_tasks = tasks - closed_tasks  # type: int

        return {
            'name': self.name,
            'description': self.description,
            'type': self.type_name,
            'tasks': tasks,
            'open_tasks': open_tasks,
            'closed_tasks': closed_tasks,
            'has_tasks': open_tasks > 0
        } class Batch(Document):
    """
    Helper category to group tasks.
    """
    id = StringField(max_length=50, primary_key=True)
    task_type = StringField(max_length=50, required=True, db_field='taskType')
    tasks_count = IntField(default=0, required=True, db_field='tasksCount')
    tasks_processed = IntField(default=0, db_field='tasksProcessed')
    closed = BooleanField(default=False, required=False)
    batch_meta = DictField(db_field='batchMeta')

    meta = {
        'collection': 'batches',
        'allow_inheritance': True,
        'indexes': [
            'task_type',
            'closed'
        ]
    }

    @classmethod
    def task_done_in(cls, batch_id: str) -> BatchUpdateResult:
        """
        Increment needed values upon a task from the batch is done. In case if
        all tasks are finished – close the batch.

        :param batch_id: Batch ID
        :type batch_id: str

        :return: Aggregate which represents complex effect of the method
        :rtype: BatchUpdateResult
        """
        num_changed = 0
        batch = cls.objects.get(id=batch_id)  # type: Batch
        processed = batch.tasks_processed + 1

        if processed > batch.tasks_count:
            return BatchUpdateResult(success=False, closed=False)

        closed = processed == batch.tasks_count
        update_q = {'inc__tasks_processed': 1}

        if closed:
            update_q['set__closed'] = closed
            num_changed = cls \
                .objects(id=batch.id, closed=False) \
                .update(**update_q)

        if num_changed == 0:
            update_q.pop('set__closed', None)
            closed = False

            num_changed = batch.update(**update_q)
        elif closed:
            on_batch_done.send(batch)

        return BatchUpdateResult(success=num_changed > 0, closed=closed)

    def __str__(self) -> str:
        return str(self.id)

    def __repr__(self) -> str:
        return 'Batch [{id}] ({processed}/{count})'.format(
            id=self.id,
            processed=self.tasks_processed,
            count=self.tasks_count) class AbstractAnswer(Document):
    """
    This is AbstractTask model.
    You need to inherit it in your model
    """
    task = ReferenceField(AbstractTask, reverse_delete_rule=CASCADE)
    created_by = ReferenceField(User, reverse_delete_rule=CASCADE,
                                db_field='createdBy')
    created_at = DateTimeField(db_field='createdAt')
    task_type = StringField(max_length=50, required=True, db_field='taskType')
    # not sure - could be extended
    result = DictField()

    meta = {
        'collection': 'reports',
        'allow_inheritance': True,
        'indexes': [
            'task',
            'created_by',
            'created_at',
            {
                'fields': ['created_by', 'task'],
                'unique': True
            }
        ]
    }

    # TODO: decide, if we need it at all
    @property
    def corrections(self) -> int:
        """
        Returns whole amount of actions/corrections given by user in this
        particular answer.

        :return: Count of corrections in this answer
        :rtype: int
        """
        return 1

    @corrections.setter
    def corrections(self, value: int) -> None:
        pass

    @corrections.deleter
    def corrections(self) -> None:
        pass

    @classmethod
    def answers_numbers_by_tasks(cls, task_ids: List[str]) -> Dict[ObjectId, int]:
        """
        Groups answers, filtered by tasks they belong to, by user and count
        number of answers for every user.

        :param task_ids: List of tasks IDs
        :type task_ids: List[str]

        :return: Map having user IDs as keys and answers numbers as values
        :rtype: Dict[ObjectId, int]
        """
        return cls.objects(task__in=task_ids).item_frequencies('created_by')

    def as_dict(self) -> Dict[str, Dict]:
        """
        Converts the model-instance into a safe that will include also task
        and user.

        :rtype: Dict[str, Dict]
        """
        return {
            'task': self.task.as_dict(),
            'answer': self.result,
            'user': self.created_by.as_dict()
        }

    def __str__(self) -> str:
        return str(self.pk)

    def __repr__(self) -> str:
        return 'Report [{} by {}]'.format(self.created_by, self.task) class AbstractTask(Document):
    """
    This is AbstractTask model.
    You need to inherit it in your model
    """
    id = StringField(max_length=200, default='', primary_key=True)
    task_type = StringField(max_length=50, required=True, db_field='taskType')
    batch = ReferenceField(Batch, reverse_delete_rule=CASCADE)

    users_count = IntField(default=0, db_field='usersCount')
    users_processed = ListField(ReferenceField(User),
                                db_field='usersProcessed')
    users_skipped = ListField(ReferenceField(User), db_field='usersSkipped')

    closed = BooleanField(default=False)
    task_data = DictField(required=True)

    meta = {
        'collection': 'tasks',
        'allow_inheritance': True,
        'indexes': [
            'task_type',
            'batch'
        ]
    }

    def as_dict(self) -> Dict[str, Any]:
        """
        Converts the model-instance into a safe and lightweight dictionary.

        :rtype: Dict[str, Any]
        """
        return {
            'id': self.id,
            'closed': self.closed,
            'data': self.task_data
        }

    @classmethod
    def ids_in_batch(cls, batch: Batch) -> List[str]:
        """
        Collects IDs of all tasks that belong to certain batch.

        :param batch: Batch instance
        :type batch: Batch

        :return: List of IDs
        :rtype: List[str]
        """
        return cls.objects(batch=batch).distinct('id')

    def __str__(self) -> str:
        return str(self.id)

    def __repr__(self) -> str:
        return str(self) class Group(Document):
    """
    Class was introduced to serve the permissions purpose
    """
    id = StringField(max_length=100, primary_key=True)
    description = StringField(max_length=200)
    allowed_types = ListField(StringField(max_length=100))

    meta = {'collection': 'groups'}

    def __str__(self) -> str:
        return str(self.id)

    def __repr__(self) -> str:
        return 'Group ID: {id}. Allowed types: {types}'.format(
            id=self.id,
            types=self.allowed_types) class User(Document, UserMixin):
    """
    Main model for member entity.
    """
    username = StringField(max_length=200)
    password = StringField(max_length=200, default='')
    name = StringField(max_length=100)
    email = StringField()
    active = BooleanField(default=True)
    admin = BooleanField(default=False)
    groups = ListField(
        ReferenceField(Group, reverse_delete_rule=PULL, default=None))
    last_login = DateTimeField(default=datetime.datetime.now)
    processed = IntField(default=0)

    def is_active(self) -> bool:
        return self.active

    def is_admin(self) -> bool:
        return self.admin or False

    def is_eligible_for(self, task_type: str) -> bool:
        """
        Check that user is authorized to work with this tasks type

        :param task_type: Tasks type name
        :type task_type: str

        :return: True if user is eligible
        :rtype: bool

        :raises: AssertionError - if no `task_type` specified
        """
        assert task_type, 'Empty parameter `task_type` passed'

        return self.admin \
               or task_type in chain(*(g.allowed_types for g in self.groups))

    def get_stats(self, task_type) -> Dict[str, int]:
        """
        Returns member's stats containing the number of tasks finished and
        the position in the global rank.

        :param task_type: Task type instance.
        :type task_type: vulyk.models.task_types.AbstractTaskType

        :return: Dictionary that contains total finished tasks count and the
                 position in the global rank.
        :rtype: Dict[str, int]
        """
        leaders = task_type.get_leaders()
        i = 0
        prev_val = -1
        total = 0

        for user, freq in leaders:
            if freq != prev_val:
                i += 1
                prev_val = freq

            if user == self.id:
                total = freq
                break

        return {
            'total': total,
            'position': i
        }

    def as_dict(self) -> Dict[str, str]:
        """
        Converts the model-instance into a safe dict that will include some
        basic info about member.

        :return: Reduced set of information about member.
        :rtype: Dict[str, str]
        """
        return {
            'username': self.username,
            'email': self.email
        }

    @classmethod
    def pre_save(cls, sender: Type, document: Document, **kwargs: Dict) -> Document:
        """
        A signal handler which will put a new member into a default group if
        any hasn't been assigned yet.

        :param sender: Type of signal emitter.
        :type sender: Type
        :param document: New instance of User model.
        :type document: User
        :param kwargs: Additional parameters
        :type kwargs: Dict

        :return: Modified User instance.
        :rtype: User
        """
        if all(map(lambda x: x.id != 'default', document.groups)):
            try:
                document.groups = [Group.objects.get(id='default')]
            except Group.DoesNotExist:
                raise Group.DoesNotExist('Please run \'manage.py init ...\'')

        return document

    @classmethod
    def get_by_id(cls, user_id: str) -> Optional[Document]:
        """
        :param user_id: Needed user ID
        :type user_id: str

        :return: The user
        :rtype: Optional[User]
        """
        try:
            return cls.objects.get(id=user_id)
        except (cls.DoesNotExist, ValidationError):
            return None

    def __str__(self) -> str:
        return self.username class BaseTest(unittest.TestCase):
    DB_NAME = 'vulyk_test'
    MONGO_URI = 'mongodb://localhost:27017/'
    # override to run against test DB during tests
    settings.MONGODB_SETTINGS['DB'] = DB_NAME
    # allows us to run selected tests w/o need to launch the whole bunch
    register_connection('default', name=DB_NAME, host=MONGO_URI) class FakeType(AbstractTaskType):
    task_model = FakeModel
    answer_model = FakeAnswer
    type_name = 'FakeTaskType'
    template = 'tmpl.html'

    _name = 'Fake name'
    _description = 'Fake description'
import gzip
import unittest
import bz2file
import click
from vulyk.cli import admin, batches, db
from vulyk.models.task_types import AbstractTaskType
from vulyk.models.tasks import Batch, AbstractAnswer, AbstractTask
from vulyk.models.user import Group, User
from .base import BaseTest
from .fixtures import FakeType
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
test_cli
"""





class TestAdmin(BaseTest):

    @classmethod
    def setUpClass(cls):
        Group(id='default',
              description='test',
              allowed_types=[FakeType.type_name]).save()

        super().setUpClass()

    @classmethod
    def tearDownClass(cls):
        Group.objects.delete()

        super().tearDownClass()

    def setUp(self):
        super().setUp()

        for i in range(1, 4):
            User(username='1',
                 email='%s@email.com' % i,
                 admin=i % 3 == 0).save()

    def tearDown(self):
        User.objects.delete()

        super().tearDown()

    def test_toggle_admin(self):
        admin.toggle_admin('1@email.com', False)

        admin.toggle_admin('2@email.com', True)