1from math import ceil
2from urllib.parse import unquote
3
4from flask import jsonify, request
5from flask_restx import inputs
6from sqlalchemy.orm.exc import NoResultFound
7
8from flexget.api import APIResource, api
9from flexget.api.app import (
10    NotFoundError,
11    base_message_schema,
12    etag,
13    pagination_headers,
14    success_response,
15)
16
17from . import db
18
19seen_api = api.namespace('seen', description='Managed Flexget seen entries and fields')
20
21
22class ObjectsContainer:
23    seen_field_object = {
24        'type': 'object',
25        'properties': {
26            'id': {'type': 'integer'},
27            'field': {'type': 'string'},
28            'value': {'type': 'string'},
29            'added': {'type': 'string', 'format': 'date-time'},
30            'seen_entry_id': {'type': 'integer'},
31        },
32    }
33
34    seen_object = {
35        'type': 'object',
36        'properties': {
37            'id': {'type': 'integer'},
38            'title': {'type': 'string'},
39            'reason': {'type': 'string'},
40            'task': {'type': 'string'},
41            'added': {'type': 'string', 'format': 'date-time'},
42            'local': {'type': 'boolean'},
43            'fields': {'type': 'array', 'items': seen_field_object},
44        },
45    }
46
47    seen_search_object = {'type': 'array', 'items': seen_object}
48
49
50seen_object_schema = api.schema_model('seen_object_schema', ObjectsContainer.seen_object)
51seen_search_schema = api.schema_model('seen_search_schema', ObjectsContainer.seen_search_object)
52
53seen_base_parser = api.parser()
54seen_base_parser.add_argument(
55    'value', help='Filter by any field value or leave empty to get all entries'
56)
57seen_base_parser.add_argument(
58    'local', type=inputs.boolean, default=None, help='Filter results by seen locality.'
59)
60
61sort_choices = ('title', 'task', 'added', 'local', 'reason', 'id')
62seen_search_parser = api.pagination_parser(seen_base_parser, sort_choices)
63
64
65@seen_api.route('/')
66class SeenSearchAPI(APIResource):
67    @etag
68    @api.response(NotFoundError)
69    @api.response(200, 'Successfully retrieved seen objects', seen_search_schema)
70    @api.doc(parser=seen_search_parser, description='Get seen entries')
71    def get(self, session):
72        """Search for seen entries"""
73        args = seen_search_parser.parse_args()
74
75        # Filter params
76        value = args['value']
77        local = args['local']
78
79        # Pagination and sorting params
80        page = args['page']
81        per_page = args['per_page']
82        sort_by = args['sort_by']
83        sort_order = args['order']
84
85        # Handle max size limit
86        if per_page > 100:
87            per_page = 100
88
89        descending = sort_order == 'desc'
90
91        # Unquotes and prepares value for DB lookup
92        if value:
93            value = unquote(value)
94            value = '%{0}%'.format(value)
95
96        start = per_page * (page - 1)
97        stop = start + per_page
98
99        kwargs = {
100            'value': value,
101            'status': local,
102            'stop': stop,
103            'start': start,
104            'order_by': sort_by,
105            'descending': descending,
106            'session': session,
107        }
108
109        total_items = db.search(count=True, **kwargs)
110
111        if not total_items:
112            return jsonify([])
113
114        raw_seen_entries_list = db.search(**kwargs).all()
115
116        converted_seen_entry_list = [entry.to_dict() for entry in raw_seen_entries_list]
117
118        # Total number of pages
119        total_pages = int(ceil(total_items / float(per_page)))
120
121        # Actual results in page
122        actual_size = min(len(converted_seen_entry_list), per_page)
123
124        # Invalid page request
125        if page > total_pages and total_pages != 0:
126            raise NotFoundError('page %s does not exist' % page)
127
128        # Get pagination headers
129        pagination = pagination_headers(total_pages, total_items, actual_size, request)
130
131        # Create response
132        rsp = jsonify(converted_seen_entry_list)
133
134        # Add link header to response
135        rsp.headers.extend(pagination)
136        return rsp
137
138    @api.response(200, 'Successfully delete all entries', model=base_message_schema)
139    @api.doc(parser=seen_base_parser, description='Delete seen entries')
140    def delete(self, session):
141        """Delete seen entries"""
142        args = seen_base_parser.parse_args()
143        value = args['value']
144        local = args['local']
145
146        if value:
147            value = unquote(value)
148            value = '%' + value + '%'
149        seen_entries_list = db.search(value=value, status=local, session=session)
150
151        deleted = 0
152        for se in seen_entries_list:
153            db.forget_by_id(se.id, session=session)
154            deleted += 1
155        return success_response('successfully deleted %i entries' % deleted)
156
157
158@seen_api.route('/<int:seen_entry_id>/')
159@api.doc(params={'seen_entry_id': 'ID of seen entry'})
160@api.response(NotFoundError)
161class SeenSearchIDAPI(APIResource):
162    @etag
163    @api.response(200, model=seen_object_schema)
164    def get(self, seen_entry_id, session):
165        """Get seen entry by ID"""
166        try:
167            seen_entry = db.get_entry_by_id(seen_entry_id, session=session)
168        except NoResultFound:
169            raise NotFoundError('Could not find entry ID {0}'.format(seen_entry_id))
170        return jsonify(seen_entry.to_dict())
171
172    @api.response(200, 'Successfully deleted entry', model=base_message_schema)
173    def delete(self, seen_entry_id, session):
174        """Delete seen entry by ID"""
175        try:
176            entry = db.get_entry_by_id(seen_entry_id, session=session)
177        except NoResultFound:
178            raise NotFoundError('Could not delete entry ID {0}'.format(seen_entry_id))
179        db.forget_by_id(entry.id, session=session)
180        return success_response('successfully deleted seen entry {}'.format(seen_entry_id))
181