~sirn/fanboi2

ref: a68873a108d20a6a49a1b6caf374bfb2cf6a94fe fanboi2/fanboi2/services/topic.py -rw-r--r-- 8.6 KiB
a68873a1Kridsada Thanabulpong Coding style cleanups and setup pre-commit hooks (#42) 3 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import datetime

from sqlalchemy.orm import contains_eager, joinedload
from sqlalchemy.sql import or_, and_, func, desc
import pytz

from ..errors import StatusRejectedError
from ..models import Board, Topic, TopicMeta, Post
from ..tasks import add_topic


class TopicCreateService(object):
    """Topic create service provides a service for creating a topic."""

    def __init__(self, dbsession, identity_svc, setting_query_svc, user_query_svc):
        self.dbsession = dbsession
        self.identity_svc = identity_svc
        self.setting_query_svc = setting_query_svc
        self.user_query_svc = user_query_svc

    def enqueue(self, board_slug, title, body, ip_address, payload):
        """Enqueues the topic creation to the posting queue. Topics that are
        queued will be processed with pre-posting filters using the given
        :param:`payload`.

        :param board_slug: The slug :type:`str` identifying a board.
        :param title: A :type:`str` topic title.
        :param body: A :type:`str` topic body.
        :param ip_address: An IP address of the topic creator.
        :param payload: A request payload containing request metadata.
        """
        return add_topic.delay(board_slug, title, body, ip_address, payload=payload)

    def _prepare_c(self, board_slug, allowed_board_status):
        """Internal method performing preparatory work to creat a new topic.
        Returns a board.

        :param board_slug: A slug :type:`str` identifying a board.
        """
        board = self.dbsession.query(Board).filter(Board.slug == board_slug).one()

        if board.status not in allowed_board_status:
            raise StatusRejectedError(board.status)

        return board

    def create(self, board_slug, title, body, ip_address):
        """Creates a new topic and associate related metadata. Unlike
        ``enqueue``, this method performs the actual creation of the topic.

        :param board_slug: A slug :type:`str` identifying a board.
        :param title: A :type:`str` topic title.
        :param body: A :type:`str` topic body.
        :param ip_address: An IP address of the topic creator.
        """
        board = self._prepare_c(board_slug, allowed_board_status=("open",))

        # Create topic

        topic = Topic(
            board=board,
            title=title,
            created_at=func.now(),
            updated_at=func.now(),
            status="open",
        )

        self.dbsession.add(topic)

        # Create topic meta

        topic_meta = TopicMeta(
            topic=topic, post_count=1, posted_at=func.now(), bumped_at=func.now()
        )

        self.dbsession.add(topic_meta)

        # Create post

        ident = None
        ident_type = "none"
        if board.settings["use_ident"]:
            time_zone = self.setting_query_svc.value_from_key("app.time_zone")
            tz = pytz.timezone(time_zone)
            timestamp = datetime.datetime.now(tz).strftime("%Y%m%d")
            ident_type = "ident"
            ident = self.identity_svc.identity_for(
                board=topic.board.slug, ip_address=ip_address, timestamp=timestamp
            )

        post = Post(
            topic=topic,
            number=topic_meta.post_count,
            body=body,
            bumped=True,
            name=board.settings["name"],
            ident=ident,
            ident_type=ident_type,
            ip_address=ip_address,
        )

        self.dbsession.add(post)
        return topic

    def create_with_user(self, board_slug, user_id, title, body, ip_address):
        """Creates a topic similar to :meth:`create` but with user ID
        associated to it.

        This method will make the post delegate ident and name from the user
        as well as allow posting in board or topic that are not archived.

        :param board_slug: A slug :type:`str` identifying a board.
        :param user_id: A user ID :type:`int` to post as.
        :param title: A :type:`str` topic title.
        :param body: A :type:`str` topic body.
        :param ip_address: An IP address of the topic creator.
        """
        user = self.user_query_svc.user_from_id(user_id)
        board = self._prepare_c(
            board_slug, allowed_board_status=("open", "restricted", "locked")
        )

        # Create topic

        topic = Topic(
            board=board,
            title=title,
            created_at=func.now(),
            updated_at=func.now(),
            status="open",
        )

        self.dbsession.add(topic)

        # Create topic meta

        topic_meta = TopicMeta(
            topic=topic, post_count=1, posted_at=func.now(), bumped_at=func.now()
        )

        self.dbsession.add(topic_meta)

        # Create post

        ident = user.ident
        ident_type = user.ident_type
        name = user.name

        post = Post(
            topic=topic,
            number=topic_meta.post_count,
            body=body,
            bumped=True,
            name=name,
            ident=ident,
            ident_type=ident_type,
            ip_address=ip_address,
        )

        self.dbsession.add(post)
        return topic


class TopicDeleteService(object):
    """Topic delete service provides a service for deleting topic and
    associated metadata.
    """

    def __init__(self, dbsession):
        self.dbsession = dbsession

    def delete(self, topic_id):
        """Delete the topic matching the given :param:`topic_id`.

        :param topic_id: An :type:`int` ID of the topic to delete.
        """
        topic = self.dbsession.query(Topic).filter_by(id=topic_id).one()

        self.dbsession.delete(topic)
        return topic


class TopicQueryService(object):
    """Topic query service provides a service for querying a topic or
    a collection of topics from the database.
    """

    def __init__(self, dbsession):
        self.dbsession = dbsession

    def _list_q(self, board_slug):
        """Internal method for querying topic list.

        :param board_slug: The slug :type:`str` identifying a board.
        """
        anchor = datetime.datetime.now() - datetime.timedelta(days=7)
        return (
            self.dbsession.query(Topic)
            .join(Topic.board, Topic.meta)
            .options(contains_eager(Topic.meta))
            .filter(
                and_(
                    Board.slug == board_slug,
                    or_(
                        Topic.status == "open",
                        and_(Topic.status != "open", TopicMeta.bumped_at >= anchor),
                    ),
                )
            )
            .order_by(desc(func.coalesce(TopicMeta.bumped_at, Topic.created_at)))
        )

    def list_from_board_slug(self, board_slug):
        """Query topics for the given board slug.

        :param board_slug: The slug :type:`str` identifying a board.
        """
        return list(self._list_q(board_slug))

    def list_recent_from_board_slug(self, board_slug, _limit=10):
        """Query recent topics for the given board slug.

        :param board_slug: The slug :type:`str` identifying a board.
        """
        return list(self._list_q(board_slug).limit(_limit))

    def list_recent(self, _limit=100):
        """Query recent topics regardless of the board."""
        anchor = datetime.datetime.now() - datetime.timedelta(days=7)
        return list(
            self.dbsession.query(Topic)
            .join(Topic.meta)
            .options(contains_eager(Topic.meta), joinedload(Topic.board))
            .filter(
                and_(
                    or_(
                        Topic.status == "open",
                        and_(Topic.status != "open", TopicMeta.bumped_at >= anchor),
                    )
                )
            )
            .order_by(desc(func.coalesce(TopicMeta.bumped_at, Topic.created_at)))
            .limit(_limit)
        )

    def topic_from_id(self, topic_id):
        """Query a topic from the given topic ID.

        :param topic_id: The ID :type:`int` identifying a topic.
        """
        return self.dbsession.query(Topic).filter_by(id=topic_id).one()


class TopicUpdateService(object):
    """Topic update service provides a service for updating topic."""

    def __init__(self, dbsession):
        self.dbsession = dbsession

    def update(self, topic_id, **kwargs):
        """Update a topic matching the given :param:`topic_id`.

        :param topic_id: The ID :type:`int` of the topic to update.
        :param **kwargs: Attributes to update.
        """
        topic = self.dbsession.query(Topic).filter_by(id=topic_id).one()

        for key in ("status",):
            if key in kwargs:
                setattr(topic, key, kwargs[key])

        self.dbsession.add(topic)
        return topic