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.
- django_siteflags-1.3.1.dist-info/METADATA +83 -0
- django_siteflags-1.3.1.dist-info/RECORD +17 -0
- django_siteflags-1.3.1.dist-info/WHEEL +4 -0
- django_siteflags-1.3.1.dist-info/licenses/LICENSE +27 -0
- siteflags/__init__.py +4 -0
- siteflags/admin.py +34 -0
- siteflags/apps.py +10 -0
- siteflags/locale/empty +0 -0
- siteflags/locale/en/LC_MESSAGES/django.po +54 -0
- siteflags/locale/ru/LC_MESSAGES/django.mo +0 -0
- siteflags/locale/ru/LC_MESSAGES/django.po +58 -0
- siteflags/migrations/0001_initial.py +38 -0
- siteflags/migrations/0002_alter_flag_content_type_alter_flag_user.py +27 -0
- siteflags/migrations/__init__.py +0 -0
- siteflags/models.py +345 -0
- siteflags/settings.py +5 -0
- siteflags/utils.py +13 -0
|
@@ -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
|
+
[](https://pypi.python.org/pypi/django-siteflags)
|
|
33
|
+
[](https://pypi.python.org/pypi/django-siteflags)
|
|
34
|
+
[](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,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
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 ""
|
|
Binary file
|
|
@@ -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
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')
|