artscoop / django-approval Goto Github PK
View Code? Open in Web Editor NEWEasy moderation of changes made to models. Django 3.2 and above, beta quality.
Home Page: https://artscoop.github.io/django-approval/
License: MIT License
Easy moderation of changes made to models. Django 3.2 and above, beta quality.
Home Page: https://artscoop.github.io/django-approval/
License: MIT License
Seems like when you install django-approval using pip install django-approval
, the main package approval
is not added to site-packages. However, I think the master branch is correct. Maybe a new push on pypi?
Hi! First, thanks for making this library!
I'm having an issue with it and m2m models using postgres and psycopg.
Here are is a relevant set of models, one of which inherits the MonitoredModel and the other of which is the Approval that is in the Sandbox.
from django.db import models
import uuid
from suppliers.models import Supplier
from django.urls import reverse
from approval.models import MonitoredModel, Sandbox, SandboxMeta
from django.conf import settings
class Bid(MonitoredModel):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.DO_NOTHING,
related_name="bids",
null=True,
blank=True,
)
is_visible = models.BooleanField(
default=True,
verbose_name="visible"
)
code = models.CharField(
max_length=24,
null=False,
blank=False,
default="BID Code",
unique=True,
)
name = models.CharField(
max_length=256,
null=True,
blank=False,
default="BID Name",
)
suppliers_invited = models.ManyToManyField(
Supplier,
related_name="suppliers_invited",
blank=True,
)
suppliers_responded = models.ManyToManyField(
Supplier,
related_name="suppliers_responded",
blank=True,
verbose_name="Suppliers who responded to the tender",
)
suppliers_technically_responsive = models.ManyToManyField(
Supplier,
related_name="suppliers_technically_responsive",
blank=True,
)
suppliers_financially_responsive = models.ManyToManyField(
Supplier,
related_name="suppliers_financially_responsive",
blank=True,
)
class Meta:
ordering = ['code']
def __str__(self):
return self.code
def get_absolute_url(self):
return reverse('bid_details', kwargs={'pk': self.id})
class BidApproval(Sandbox, metaclass=SandboxMeta):
base = Bid
approval_fields = ['code', 'name',
'suppliers_invited',
'suppliers_responded',
'suppliers_technically_responsive',
'suppliers_financially_responsive',
]
approval_default = {'is_visible': False, }
auto_approve_staff = False
auto_approve_new = False
auto_approve_by_request = False
delete_on_approval = False
def _get_authors(self):
return [self.source.user]
Here is the output from the interactive shell when I create and try to save a Bid that inherits the MonitoredModel class. First, the relevant content:
>>> from bids.models import *
>>> test = Bid(name="test", code="test")
>>> all_bids = Bid.objects.all()
>>> print(all_bids)
<QuerySet []>
>>> test.save()
Traceback (most recent call last):
...
File "/usr/local/lib/python3.12/site-packages/approval/listeners/monitored.py", line 37, in before_save
instance.approval._update_sandbox()
File "/usr/local/lib/python3.12/site-packages/approval/models/monitoring.py", line 215, in _update_sandbox
self.save()
File "/usr/local/lib/python3.12/site-packages/approval/models/monitoring.py", line 117, in save
return super().save(**options)
^^^^^^^^^^^^^^^^^^^^^^^
...
Now, the full error message:
>>> from bids.models import *
>>> test = Bid(name="test", code="test")
>>> all_bids = Bid.objects.all()
>>> print(all_bids)
<QuerySet []>
>>> test.save()
Traceback (most recent call last):
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 114, in debug_sql
yield
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 102, in execute
return super().execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 67, in execute
return self._execute_with_wrappers(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
return executor(sql, params, many, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 89, in _execute
return self.cursor.execute(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/cursor.py", line 728, in execute
self._conn.wait(
File "/usr/local/lib/python3.12/site-packages/psycopg/connection.py", line 969, in wait
return waiting.wait(gen, self.pgconn.socket, timeout=timeout)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "psycopg_binary/_psycopg/waiting.pyx", line 190, in psycopg_binary._psycopg.wait_c
File "/usr/local/lib/python3.12/site-packages/psycopg/cursor.py", line 210, in _execute_gen
pgq = self._convert_query(query, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/client_cursor.py", line 79, in _convert_query
pgq.convert(query, params)
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 213, in convert
self.dump(vars)
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 223, in dump
self.params = tuple(
^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 224, in <genexpr>
self._tx.as_literal(p) if p is not None else b"NULL" for p in params
^^^^^^^^^^^^^^^^^^^^^^
File "psycopg_binary/_psycopg/transform.pyx", line 206, in psycopg_binary._psycopg.Transformer.as_literal
File "psycopg_binary/_psycopg/transform.pyx", line 215, in psycopg_binary._psycopg.Transformer.as_literal
File "/usr/local/lib/python3.12/site-packages/psycopg/adapt.py", line 57, in quote
value = self.dump(obj)
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/types/json.py", line 151, in dump
data = dumps(obj)
^^^^^^^^^^
File "/usr/local/lib/python3.12/json/__init__.py", line 238, in dumps
**kw).encode(obj)
^^^^^^^^^^^
File "/usr/local/lib/python3.12/json/encoder.py", line 200, in encode
chunks = self.iterencode(o, _one_shot=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/json/encoder.py", line 258, in iterencode
return _iterencode(o, 0)
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/core/serializers/json.py", line 106, in default
return super().default(o)
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/json/encoder.py", line 180, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
**TypeError: Object of type ManyRelatedManager is not JSON serializable**
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 814, in save
self.save_base(
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 861, in save_base
pre_save.send(
File "/usr/local/lib/python3.12/site-packages/django/dispatch/dispatcher.py", line 177, in send
(receiver, receiver(signal=self, sender=sender, **named))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
**_File "/usr/local/lib/python3.12/site-packages/approval/listeners/monitored.py", line 37, in before_save
instance.approval._update_sandbox()
File "/usr/local/lib/python3.12/site-packages/approval/models/monitoring.py", line 215, in _update_sandbox
self.save()
File "/usr/local/lib/python3.12/site-packages/approval/models/monitoring.py", line 117, in save
return super().save(**options)
^^^^^^^^^^^^^^^^^^^^^^^_**
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 814, in save
self.save_base(
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 877, in save_base
updated = self._save_table(
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 1020, in _save_table
results = self._do_insert(
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/models/base.py", line 1061, in _do_insert
return manager._insert(
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/models/manager.py", line 87, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/models/query.py", line 1805, in _insert
return query.get_compiler(using=using).execute_sql(returning_fields)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1822, in execute_sql
cursor.execute(sql, params)
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 101, in execute
with self.debug_sql(sql, params, use_last_executed_query=True):
File "/usr/local/lib/python3.12/contextlib.py", line 158, in __exit__
self.gen.throw(value)
File "/usr/local/lib/python3.12/site-packages/django/db/backends/utils.py", line 119, in debug_sql
sql = self.db.ops.last_executed_query(self.cursor, sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/postgresql/operations.py", line 301, in last_executed_query
return self.compose_sql(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/postgresql/operations.py", line 194, in compose_sql
return mogrify(sql, params, self.connection)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/db/backends/postgresql/psycopg_any.py", line 22, in mogrify
return ClientCursor(cursor.connection).mogrify(sql, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/client_cursor.py", line 40, in mogrify
pgq = self._convert_query(query, params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/client_cursor.py", line 79, in _convert_query
pgq.convert(query, params)
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 213, in convert
self.dump(vars)
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 223, in dump
self.params = tuple(
^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/_queries.py", line 224, in <genexpr>
self._tx.as_literal(p) if p is not None else b"NULL" for p in params
^^^^^^^^^^^^^^^^^^^^^^
File "psycopg_binary/_psycopg/transform.pyx", line 206, in psycopg_binary._psycopg.Transformer.as_literal
File "psycopg_binary/_psycopg/transform.pyx", line 215, in psycopg_binary._psycopg.Transformer.as_literal
File "/usr/local/lib/python3.12/site-packages/psycopg/adapt.py", line 57, in quote
value = self.dump(obj)
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/psycopg/types/json.py", line 151, in dump
data = dumps(obj)
^^^^^^^^^^
File "/usr/local/lib/python3.12/json/__init__.py", line 238, in dumps
**kw).encode(obj)
^^^^^^^^^^^
File "/usr/local/lib/python3.12/json/encoder.py", line 200, in encode
chunks = self.iterencode(o, _one_shot=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/json/encoder.py", line 258, in iterencode
return _iterencode(o, 0)
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/django/core/serializers/json.py", line 106, in default
return super().default(o)
^^^^^^^^^^^^^^^^^^
**File "/usr/local/lib/python3.12/json/encoder.py", line 180, in default
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type ManyRelatedManager is not JSON serializable**
I'm unsure if this is an error that others are facing when trying to monitor models with m2m relations.
Relevant contents of my Pipfile:
django = "==4.2.11"
psycopg = {extras = ["binary"] }
django-approval = "*"
Looks like the save() method in the DynamicSandbox class might be the culprit.
Is there a way to add a check to see if the model has any attributes that are m2m and if so, pass those fields off to separate methods that serialize and deserialize them by grabbing each related model's pk, storing them in a dictionary one-by-one (maybe changing the key so that it appends '-#' to the end of each key), adding that dictionary of related objects and their PKs into the JSON object, and then doing the reverse in the _update_source method?
Alternatively, if this is a known issue - or if this is something that might be unique to my setup - please let me know.
I dont know if you got my other response in the old ticket since its closed so im making a new one just in case:
The change is incorrect. The new if-statement leads to the else block if the model is approved.
And the else block shows an "ImproperlyConfigured" error.
The solution is to move the last check down into the if-statement.
Replace
if hasattr(obj, "approval") and obj.approval and not obj.approval.approved:
with
if hasattr(obj, "approval") and obj.approval:
if obj.approval.approved != True:
(like it was in my original solution provided in the first post in this issue)
Need to document the currently simple way of using this package.
is there a way to automatically delete an approval when it was approved?
OR
is there a way to only show the "This form is showing changes currently pending." warning if the approval has NOT been approved?
currently it shows even if the approval is approved because the code only checks IF there is an approval object but not if its approved or not.
suggested change:
monitored.py line 23:
if hasattr(obj, "approval") and obj.approval:
if obj.approval.approved != True: // added check
I guess this library was specifically made to have approvals in Django forms?
Is there a way to
is_visible
to allow for custom logic for approval e.g. change enum from AWAITING_APPROVAL to PENDINGThanks for making this library!
Maybe a non-issue, but before I added this, everything created/updated was skipping the Sandbox and the monitored model was being saved directly without requiring approval.
Using django 4.2.
Thanks for making this!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.