django-siteflags 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-siteflags
3
+ Version: 1.3.1
4
+ Summary: Reusable application for Django allowing users to flag/bookmark site objects
5
+ Project-URL: Homepage, https://github.com/idlesign/django-siteflags
6
+ Project-URL: Documentation, https://django-siteflags.readthedocs.io/
7
+ Author-email: Igor Starikov <idlesign@yandex.ru>
8
+ License-Expression: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Keywords: Django,bookmarks,flags
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Web Environment
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: django-etc>=1.2.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # django-siteflags
29
+
30
+ <https://github.com/idlesign/django-siteflags>
31
+
32
+ [![release](https://img.shields.io/pypi/v/django-siteflags.svg)](https://pypi.python.org/pypi/django-siteflags)
33
+ [![lic](https://img.shields.io/pypi/l/django-siteflags.svg)](https://pypi.python.org/pypi/django-siteflags)
34
+ [![coverage](https://img.shields.io/coveralls/idlesign/django-siteflags/master.svg)](https://coveralls.io/r/idlesign/django-siteflags)
35
+
36
+ ## Description
37
+
38
+ *Reusable application for Django allowing users to flag/bookmark site objects*
39
+
40
+ So you want a user to be able to put some flags on certain site entities.
41
+
42
+ Let's say you need a kind of bookmark powered service, or a site where content is flagged and moderated,
43
+ or a simplified rating system, or something similar.
44
+
45
+ Inherit you model from `siteflags.models.ModelWithFlag` and you're almost done.
46
+
47
+ Like that:
48
+
49
+ ```python
50
+ # myapp/models.py
51
+ from siteflags.models import ModelWithFlag
52
+
53
+
54
+ class Article(ModelWithFlag):
55
+
56
+ ... # Some model fields here.
57
+ ```
58
+
59
+ And like so:
60
+
61
+ ```python
62
+ # myapp/views.py
63
+ from django.shortcuts import get_object_or_404
64
+ from .models import Article
65
+
66
+
67
+ def article_details(request, article_id):
68
+
69
+ article = get_object_or_404(Article, pk=article_id)
70
+
71
+ user = request.user
72
+ article.set_flag(user)
73
+ article.is_flagged(user)
74
+ article.remove_flag(user)
75
+
76
+ ...
77
+ ```
78
+
79
+ Quite simple. Quite generic. Read the documentation.
80
+
81
+ ## Documentation
82
+
83
+ <https://django-siteflags.readthedocs.io/>
@@ -0,0 +1,17 @@
1
+ siteflags/__init__.py,sha256=LhfcrliOSRBaoBG63tAozrhVePp8WjbNdR8v8M6Nriw,74
2
+ siteflags/admin.py,sha256=nktGm6rQCPltJ-eo80yHHk4v6imbmpNytpzyH_7cO70,553
3
+ siteflags/apps.py,sha256=8_Vp500cVWsSDGCPov8tRhtPBLrRqZYL9oYv4JJMY14,273
4
+ siteflags/models.py,sha256=Qt4-l2eYBXoSughKCsFTsmzRk5-9BSQbxSQMef48p7E,10587
5
+ siteflags/settings.py,sha256=_P-eurszeYAXp4cr10CfSW3Na7fWVAThL5Lomf0NYdY,173
6
+ siteflags/utils.py,sha256=luwjqlIyXVVXFIvoU83kBUHib8ibp6T2OAYxDqkMns4,330
7
+ siteflags/locale/empty,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ siteflags/locale/en/LC_MESSAGES/django.po,sha256=vGGulGisJaGjzwrDwXeOtuFagzn5syNRSq0_RicR8JU,969
9
+ siteflags/locale/ru/LC_MESSAGES/django.mo,sha256=0Sxep1PgIo1df4_rWs1QcrnNX8cGImsw_h8ovDipyu4,1028
10
+ siteflags/locale/ru/LC_MESSAGES/django.po,sha256=5K19uYUaIoTh2sZFwtjfr3V_Sv9NFSn3fbBqzq1kisU,1376
11
+ siteflags/migrations/0001_initial.py,sha256=5E04N8Uyogd4197fmMFXeLi8nWW-OenEI1Twe1n8JSo,1633
12
+ siteflags/migrations/0002_alter_flag_content_type_alter_flag_user.py,sha256=AMuQWe8V-cXQANcLS7T_avUt30lP2aC2MX_ulmSpiKU,972
13
+ siteflags/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ django_siteflags-1.3.1.dist-info/METADATA,sha256=Nb2q8NV1CWCT8JCtXiW9mmrFiJPxMbF5mhmszm7pKto,2585
15
+ django_siteflags-1.3.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
16
+ django_siteflags-1.3.1.dist-info/licenses/LICENSE,sha256=8VxXkHRjdSGZbcDPHxL73V2tdQdhLnyka9u45l2FAl8,1508
17
+ django_siteflags-1.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2014-2026, Igor `idle sign` Starikov
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the django-siteflags nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
siteflags/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ VERSION = '1.3.1'
2
+
3
+
4
+ default_app_config = 'siteflags.apps.SiteflagsConfig'
siteflags/admin.py ADDED
@@ -0,0 +1,34 @@
1
+ from django.contrib import admin
2
+
3
+ from .utils import get_flag_model
4
+
5
+ FLAG_MODEL = get_flag_model()
6
+
7
+
8
+ @admin.register(FLAG_MODEL)
9
+ class FlagModelAdmin(admin.ModelAdmin):
10
+
11
+ list_display = (
12
+ 'time_created',
13
+ 'content_type',
14
+ 'object_id',
15
+ 'status',
16
+ )
17
+
18
+ search_fields = (
19
+ 'object_id',
20
+ 'content_type',
21
+ 'user',
22
+ )
23
+
24
+ list_filter = (
25
+ 'time_created',
26
+ 'status',
27
+ 'content_type',
28
+ )
29
+
30
+ ordering = (
31
+ '-time_created',
32
+ )
33
+
34
+ date_hierarchy = 'time_created'
siteflags/apps.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class SiteflagsConfig(AppConfig):
6
+ """Siteflags configuration."""
7
+
8
+ name = 'siteflags'
9
+ verbose_name = _('Site Flags')
10
+ default_auto_field = 'django.db.models.AutoField'
siteflags/locale/empty ADDED
File without changes
@@ -0,0 +1,54 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PACKAGE VERSION\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2014-09-08 14:33+0700\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+
20
+ #: config.py:9
21
+ msgid "Site Flags"
22
+ msgstr ""
23
+
24
+ #: models.py:28
25
+ msgid "Note"
26
+ msgstr ""
27
+
28
+ #: models.py:29
29
+ msgid "Status"
30
+ msgstr ""
31
+
32
+ #: models.py:31
33
+ msgid "User"
34
+ msgstr ""
35
+
36
+ #: models.py:32
37
+ msgid "Date created"
38
+ msgstr ""
39
+
40
+ #: models.py:35
41
+ msgid "Object ID"
42
+ msgstr ""
43
+
44
+ #: models.py:36
45
+ msgid "Content type"
46
+ msgstr ""
47
+
48
+ #: models.py:42
49
+ msgid "Flag"
50
+ msgstr ""
51
+
52
+ #: models.py:43
53
+ msgid "Flags"
54
+ msgstr ""
@@ -0,0 +1,58 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ #
5
+ # Translators:
6
+ # Igor Starikov <idlesign@yandex.ru>, 2014
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: django-siteflags\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2014-09-08 14:33+0700\n"
12
+ "PO-Revision-Date: 2014-09-08 14:35+0700\n"
13
+ "Last-Translator: Igor 'idle sign' Starikov <idlesign@yandex.ru>\n"
14
+ "Language-Team: Russian (http://www.transifex.com/projects/p/django-siteflags/"
15
+ "language/ru/)\n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Language: ru\n"
20
+ "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
21
+ "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
22
+ "X-Generator: Poedit 1.5.4\n"
23
+
24
+ #: config.py:9
25
+ msgid "Site Flags"
26
+ msgstr "Флаги сайта"
27
+
28
+ #: models.py:28
29
+ msgid "Note"
30
+ msgstr "Заметка"
31
+
32
+ #: models.py:29
33
+ msgid "Status"
34
+ msgstr "Статус"
35
+
36
+ #: models.py:31
37
+ msgid "User"
38
+ msgstr "Пользователь"
39
+
40
+ #: models.py:32
41
+ msgid "Date created"
42
+ msgstr "Дата создания"
43
+
44
+ #: models.py:35
45
+ msgid "Object ID"
46
+ msgstr "ID объекта"
47
+
48
+ #: models.py:36
49
+ msgid "Content type"
50
+ msgstr "Тип содержимого"
51
+
52
+ #: models.py:42
53
+ msgid "Flag"
54
+ msgstr "Флаг"
55
+
56
+ #: models.py:43
57
+ msgid "Flags"
58
+ msgstr "Флаги"
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import unicode_literals
3
+
4
+ from django.db import models, migrations
5
+ from django.conf import settings
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12
+ ('contenttypes', '0001_initial'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='Flag',
18
+ fields=[
19
+ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20
+ ('note', models.TextField(verbose_name='Note', blank=True)),
21
+ ('status', models.IntegerField(db_index=True, null=True, verbose_name='Status', blank=True)),
22
+ ('time_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
23
+ ('object_id', models.PositiveIntegerField(verbose_name='Object ID', db_index=True)),
24
+ ('content_type', models.ForeignKey(related_name='siteflags_flag_flags', verbose_name='Content type', to='contenttypes.ContentType', on_delete=models.CASCADE)),
25
+ ('user', models.ForeignKey(related_name='flag_users', verbose_name='User', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
26
+ ],
27
+ options={
28
+ 'abstract': False,
29
+ 'verbose_name': 'Flag',
30
+ 'verbose_name_plural': 'Flags',
31
+ },
32
+ bases=(models.Model,),
33
+ ),
34
+ migrations.AlterUniqueTogether(
35
+ name='flag',
36
+ unique_together=set([('content_type', 'object_id', 'user', 'status')]),
37
+ ),
38
+ ]
@@ -0,0 +1,27 @@
1
+ # Generated by Django 6.1.3 on 2026-11-10 07:51
2
+
3
+ import django.db.models.deletion
4
+ from django.conf import settings
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ('contenttypes', '0002_remove_content_type_name'),
12
+ ('siteflags', '0001_initial'),
13
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14
+ ]
15
+
16
+ operations = [
17
+ migrations.AlterField(
18
+ model_name='flag',
19
+ name='content_type',
20
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_flags', to='contenttypes.contenttype', verbose_name='Content type'),
21
+ ),
22
+ migrations.AlterField(
23
+ model_name='flag',
24
+ name='user',
25
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_users', to=settings.AUTH_USER_MODEL, verbose_name='User'),
26
+ ),
27
+ ]
File without changes
siteflags/models.py ADDED
@@ -0,0 +1,345 @@
1
+ from collections import defaultdict
2
+ from typing import List, Type, Dict, Union, Tuple, Optional
3
+ from collections.abc import Sequence
4
+
5
+ from django.conf import settings
6
+ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
7
+ from django.contrib.contenttypes.models import ContentType
8
+ from django.db import models, IntegrityError
9
+ from django.db.models.query import QuerySet
10
+ from django.utils.translation import gettext_lazy as _
11
+ from etc.toolbox import get_model_class_from_string
12
+
13
+ from .settings import MODEL_FLAG
14
+ from .utils import get_flag_model
15
+
16
+ if False: # pragma: nocover
17
+ from django.contrib.auth.models import User # noqa
18
+
19
+ USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
20
+
21
+ TypeFlagsForType = list['FlagBase']
22
+ TypeFlagsForTypes = dict[type[models.Model], TypeFlagsForType]
23
+
24
+
25
+ class FlagBase(models.Model):
26
+ """Base class for flag models.
27
+ Flags are marks on various site entities (model instances).
28
+
29
+ Inherit from this model and override SITEFLAGS_FLAG_MODEL in settings.py
30
+ to customize model fields and behaviour.
31
+
32
+ """
33
+ note = models.TextField(_('Note'), blank=True)
34
+ status = models.IntegerField(_('Status'), null=True, blank=True, db_index=True)
35
+
36
+ user = models.ForeignKey(
37
+ USER_MODEL, related_name='%(class)s_users', verbose_name=_('User'),
38
+ on_delete=models.CASCADE)
39
+
40
+ time_created = models.DateTimeField(_('Date created'), auto_now_add=True)
41
+
42
+ # Here follows a link to an object.
43
+ object_id = models.PositiveIntegerField(verbose_name=_('Object ID'), db_index=True)
44
+
45
+ content_type = models.ForeignKey(
46
+ ContentType, verbose_name=_('Content type'),
47
+ related_name='%(app_label)s_%(class)s_flags',
48
+ on_delete=models.CASCADE)
49
+
50
+ linked_object = GenericForeignKey()
51
+
52
+ class Meta:
53
+
54
+ abstract = True
55
+
56
+ verbose_name = _('Flag')
57
+ verbose_name_plural = _('Flags')
58
+
59
+ unique_together = (
60
+ 'content_type',
61
+ 'object_id',
62
+ 'user',
63
+ 'status',
64
+ )
65
+
66
+ @classmethod
67
+ def get_flags_for_types(
68
+ cls,
69
+ mdl_classes: list[type[models.Model]],
70
+ *,
71
+ user: 'User' = None,
72
+ status: int = None,
73
+ allow_empty: bool = True,
74
+ with_objects: bool = False,
75
+
76
+ ) -> TypeFlagsForTypes:
77
+ """Returns a dictionary with flag objects associated with the given model classes (types).
78
+ The dictionary is indexed by model classes.
79
+ Each dict entry contains a list of associated flags.
80
+
81
+ :param mdl_classes: Types to get flags for.
82
+ :param user: User filter,
83
+ :param status: Status filter
84
+ :param allow_empty: Flag. Include results for all given types, even those without associated flags.
85
+ :param with_objects: Whether to fetch the flagged objects along with the flags.
86
+
87
+ """
88
+ if not mdl_classes or (user and not user.id):
89
+ return {}
90
+
91
+ types_for_models = ContentType.objects.get_for_models(*mdl_classes, for_concrete_models=False)
92
+ filter_kwargs = {'content_type__in': types_for_models.values()}
93
+ update_filter_dict(filter_kwargs, user=user, status=status)
94
+
95
+ flags = cls.objects.filter(**filter_kwargs)
96
+
97
+ if with_objects:
98
+ flags = flags.prefetch_related('linked_object')
99
+
100
+ flags = flags.order_by('-time_created')
101
+
102
+ flags_dict = defaultdict(list)
103
+
104
+ for flag in flags:
105
+ flags_dict[flag.content_type_id].append(flag)
106
+
107
+ result = {} # Respect initial order.
108
+
109
+ for mdl_cls in mdl_classes:
110
+
111
+ content_type_id = types_for_models[mdl_cls].id
112
+
113
+ if content_type_id in flags_dict:
114
+ result[mdl_cls] = flags_dict[content_type_id]
115
+
116
+ elif allow_empty:
117
+ result[mdl_cls] = []
118
+
119
+ return result
120
+
121
+ @classmethod
122
+ def get_flags_for_objects(
123
+ cls,
124
+ objects_list: Union[QuerySet, Sequence],
125
+ *,
126
+ user: 'User' = None,
127
+ status: int = None
128
+
129
+ ) -> dict[int, TypeFlagsForType]:
130
+ """Returns a dictionary with flag objects associated with the given model objects.
131
+ The dictionary is indexed by objects IDs.
132
+ Each dict entry contains a list of associated flag objects.
133
+
134
+ :param objects_list:
135
+ :param user:
136
+ :param status:
137
+
138
+ """
139
+ if not objects_list or (user and not user.id):
140
+ return {}
141
+
142
+ objects_ids = objects_list
143
+ if not isinstance(objects_list, QuerySet):
144
+ objects_ids = [obj.pk for obj in objects_list]
145
+
146
+ filter_kwargs = {
147
+ 'object_id__in': objects_ids,
148
+ # Consider this list homogeneous.
149
+ 'content_type': ContentType.objects.get_for_model(objects_list[0], for_concrete_model=False)
150
+ }
151
+ update_filter_dict(filter_kwargs, user=user, status=status)
152
+
153
+ flags = cls.objects.filter(**filter_kwargs)
154
+ flags_dict = defaultdict(list)
155
+
156
+ for flag in flags:
157
+ flags_dict[flag.object_id].append(flag)
158
+
159
+ result = {}
160
+
161
+ for obj in objects_list:
162
+ result[obj.pk] = flags_dict.get(obj.pk, [])
163
+
164
+ return result
165
+
166
+ def __str__(self):
167
+ return f'{self.content_type}:{self.object_id} status {self.status}'
168
+
169
+
170
+ class Flag(FlagBase):
171
+ """Built-in flag class. Default functionality."""
172
+
173
+
174
+ class ModelWithFlag(models.Model):
175
+ """Helper base class for models with flags.
176
+
177
+ Inherit from this model to be able to mark model instances.
178
+
179
+ """
180
+ flags = GenericRelation(MODEL_FLAG)
181
+
182
+ class Meta:
183
+ abstract = True
184
+
185
+ @classmethod
186
+ def get_flags_for_type(
187
+ cls,
188
+ mdl_classes: list[type[models.Model]] = None,
189
+ *,
190
+ user: 'User' = None,
191
+ status: int = None,
192
+ allow_empty: bool = True,
193
+ with_objects: bool = False,
194
+
195
+ ) -> Union[TypeFlagsForTypes, TypeFlagsForType]:
196
+ """Returns a dictionary with flag objects associated with
197
+ the given model classes (types) if mdl_classes is given.
198
+ The dictionary is indexed by model classes.
199
+ Each dict entry contains a list of associated flag objects.
200
+
201
+ If mdl_classes is not given, returns a list of associated
202
+ flag objects for this very class.
203
+
204
+ :param mdl_classes: Types to get flags for. If not set the current class is used.
205
+ :param user: User filter,
206
+ :param status: Status filter
207
+ :param allow_empty: Flag. Include results for all given types, even those without associated flags.
208
+ :param with_objects: Whether to fetch the flagged objects along with the flags.
209
+
210
+ """
211
+ model: FlagBase = get_model_class_from_string(MODEL_FLAG)
212
+
213
+ single_type = False
214
+ if mdl_classes is None:
215
+ mdl_classes = [cls]
216
+ single_type = True
217
+ allow_empty = True
218
+
219
+ result = model.get_flags_for_types(
220
+ mdl_classes,
221
+ user=user,
222
+ status=status,
223
+ allow_empty=allow_empty,
224
+ with_objects=with_objects,
225
+ )
226
+
227
+ if single_type:
228
+ result = result[cls]
229
+
230
+ return result
231
+
232
+ get_flags_for_types = get_flags_for_type # alias
233
+
234
+ @classmethod
235
+ def get_flags_for_objects(
236
+ cls,
237
+ objects_list: Union[QuerySet, Sequence],
238
+ *,
239
+ user: 'User' = None,
240
+ status: int = None
241
+
242
+ ) -> dict[int, TypeFlagsForType]:
243
+ """Returns a dictionary with flag objects associated with the given model objects.
244
+ The dictionary is indexed by objects IDs.
245
+ Each dict entry contains a list of associated flag objects.
246
+
247
+ :param objects_list:
248
+ :param user:
249
+ :param status:
250
+
251
+ """
252
+ model: FlagBase = get_model_class_from_string(MODEL_FLAG)
253
+ return model.get_flags_for_objects(objects_list, user=user, status=status)
254
+
255
+ def get_flags(self, user: 'User' = None, *, status: int = None) -> Union[QuerySet, Sequence[FlagBase]]:
256
+ """Returns flags for the object optionally filtered by status.
257
+
258
+ :param user: Optional user filter
259
+ :param status: Optional status filter
260
+
261
+ """
262
+ filter_kwargs = {}
263
+ update_filter_dict(filter_kwargs, user=user, status=status)
264
+ return self.flags.filter(**filter_kwargs).all()
265
+
266
+ def set_flag(self, user: 'User', *, note: str = None, status: int = None) -> FlagBase | None:
267
+ """Flags the object.
268
+
269
+ :param user:
270
+ :param note: User-defined note for this flag.
271
+ :param status: Optional status integer (the meaning is defined by a developer).
272
+
273
+ """
274
+ if not user.id:
275
+ return None
276
+
277
+ init_kwargs = {
278
+ 'user': user,
279
+ 'linked_object': self,
280
+ }
281
+ if note is not None:
282
+ init_kwargs['note'] = note
283
+
284
+ if status is not None:
285
+ init_kwargs['status'] = status
286
+
287
+ flag = get_flag_model()(**init_kwargs)
288
+
289
+ try:
290
+ flag.save()
291
+
292
+ except IntegrityError: # Record already exists.
293
+ return None
294
+
295
+ return flag
296
+
297
+ def remove_flag(self, user: 'User' = None, *, status: int = None):
298
+ """Removes flag(s) from the object.
299
+
300
+ :param user: Optional user filter
301
+ :param status: Optional status filter
302
+
303
+ """
304
+ filter_kwargs = {
305
+ 'content_type': ContentType.objects.get_for_model(self),
306
+ 'object_id': self.id
307
+ }
308
+ update_filter_dict(filter_kwargs, user=user, status=status)
309
+ get_flag_model().objects.filter(**filter_kwargs).delete()
310
+
311
+ def is_flagged(self, user: 'User' = None, *, status: int = None) -> int:
312
+ """Returns a number of times the object is flagged by a user.
313
+
314
+ :param user: Optional user filter
315
+ :param status: Optional status filter
316
+
317
+ """
318
+ if user and user.is_anonymous:
319
+ return False
320
+
321
+ filter_kwargs = {
322
+ 'content_type': ContentType.objects.get_for_model(self),
323
+ 'object_id': self.id,
324
+ }
325
+ update_filter_dict(filter_kwargs, user=user, status=status)
326
+ return self.flags.filter(**filter_kwargs).count()
327
+
328
+
329
+ def update_filter_dict(d: dict, *, user: Optional['User'], status: int | None):
330
+ """Helper. Updates filter dict for a queryset.
331
+
332
+ :param d:
333
+ :param user:
334
+ :param status:
335
+
336
+ """
337
+ if user is not None:
338
+
339
+ if not user.id:
340
+ return None
341
+
342
+ d['user'] = user
343
+
344
+ if status is not None:
345
+ d['status'] = status
siteflags/settings.py ADDED
@@ -0,0 +1,5 @@
1
+ from django.conf import settings
2
+
3
+
4
+ MODEL_FLAG = getattr(settings, 'SITEFLAGS_FLAG_MODEL', 'siteflags.Flag')
5
+ """Dotted path to a Flag custom model in form of `app.Model`."""
siteflags/utils.py ADDED
@@ -0,0 +1,13 @@
1
+ from typing import Type
2
+
3
+ from etc.toolbox import get_model_class_from_settings
4
+
5
+ from siteflags import settings
6
+
7
+ if False: # pragma: nocover
8
+ from .models import Flag
9
+
10
+
11
+ def get_flag_model() -> type['Flag']:
12
+ """Returns the Flag model, set for the project."""
13
+ return get_model_class_from_settings(settings, 'MODEL_FLAG')