Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filtering on the deleted field is done on evaluation. #65

Merged
merged 4 commits into from
Apr 3, 2017

Conversation

AndreasBackx
Copy link
Contributor

See #64 for extra information (this PR is branched from #64), but here's a summary:

  • SafeDeleteMixin.refresh_from_db works if visibility is set to DELETED_VISIBLE_BY_FIELD.
  • SafeDeleteMixin.objects.filter(pk={pk}).get() does not give a DoesNotExist if visibility is set to DELETED_VISIBLE_BY_FIELD. This is why refresh_from_db works now. If you have any idea on how to make this work for the other ones, feel free to leave some feedback. I cannot come up with a good solution as they would have race conditions.
  • I initially thought that SafeDeleteMixin.objects.select_related('something').get(pk={pk}) would ignore the select_related because get would create a new queryset. It apparently didn't in my initial tests which still boggles me. Anyhow, added tests assures it doesn't now.
  • All QuerySet specific methods moved to SafeDeleteQueryset.

So the filter(deleted__isnull=True/False) is added when the QuerySet is evaluated. See __getattribute__ which intercepts those calls to add it. Would love some feedback, the added code has got documentation so check those too.

return queryset

def all_with_deleted(self):
"""Deprecated because all(show_deleted=True) is meant for related managers."""
warnings.warn('deprecated', DeprecationWarning)
Copy link
Contributor Author

@AndreasBackx AndreasBackx Feb 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better if we not deprecate this, but add helper methods for all visibility modifiers. We should perhaps add a note with rst stating that this should only be used for related fields.

)
if hasattr(attr, '__call__') and name in evaluation_methods:
def decorator(*args, **kwargs):
self.filter_visibility()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be documented in the docs for when using custom QuerySets. When working with the QuerySet's query directly that hasn't had the deleted filter applied yet, filter_visibility needs to be called first.

@coveralls
Copy link

coveralls commented Feb 5, 2017

Coverage Status

Coverage increased (+0.1%) to 97.974% when pulling 5d15cef on AndreasBackx:enhancement/queryset-reuse into ec0f547 on makinacorpus:master.

@coveralls
Copy link

coveralls commented Feb 5, 2017

Coverage Status

Coverage increased (+0.1%) to 97.974% when pulling 5d15cef on AndreasBackx:enhancement/queryset-reuse into ec0f547 on makinacorpus:master.

@AndreasBackx
Copy link
Contributor Author

AndreasBackx commented Feb 28, 2017

This and #64 are ready to be merged if everything looks good to you. I moved the SafeDeleteQueryset to queryset.py and added docs there too. It remains backwards compatible and can still be imported with from safedelete.managers import SafeDeleteQueryset.

@Gagaro
Copy link
Member

Gagaro commented Mar 1, 2017

Thanks a lot 👍 . If you could just add a changelog in the CHANGES file it would be perfect!

@Gagaro
Copy link
Member

Gagaro commented Mar 1, 2017

So I should close #64 and merge #65, right?

@AndreasBackx
Copy link
Contributor Author

AndreasBackx commented Mar 1, 2017

@Gagaro I don't really mind how you merge it. Might be easier to close the first one and refer to this one saying that this one is merged. But if you still want to merge both, I guess you should be fine if you don't create a merge commit.

I'll add the changes to the changelog tonight. Do you want a version bump then...?

@Gagaro
Copy link
Member

Gagaro commented Mar 2, 2017

Don't worry about the version, I'll take care of it if I release it.

@Gagaro Gagaro merged commit 4f4d29e into makinacorpus:master Apr 3, 2017
Gagaro added a commit that referenced this pull request Apr 3, 2017
@glemmaPaul
Copy link
Contributor

Hey guys!

It seems that this code change gave me an issue, as following:

User.objects.filter(email="paul@email.nl").first()

Gives me:

File "/Users/pauloostenrijk/webprojects/app/app/users/import_export/resources.py", line 57, in before_import
user = User.objects.all().filter(email=row['email']).first()
File "/Users/pauloostenrijk/.virtualenvs/app/lib/python2.7/site-packages/django/db/models/query.py", line 550, in first
objects = list((self if self.ordered else self.order_by('pk'))[:1])
File "/Users/pauloostenrijk/.virtualenvs/app/lib/python2.7/site-packages/django/db/models/query.py", line 258, in __iter__
self._fetch_all()
File "/Users/pauloostenrijk/.virtualenvs/app/lib/python2.7/site-packages/safedelete/queryset.py", line 121, in decorator
self._filter_visibility()
File "/Users/pauloostenrijk/.virtualenvs/app/lib/python2.7/site-packages/safedelete/queryset.py", line 98, in _filter_visibility
"Cannot filter a query once a slice has been taken."
AssertionError: Cannot filter a query once a slice has been taken.

@glemmaPaul
Copy link
Contributor

glemmaPaul commented Apr 25, 2017

Looking at the source code:

        """Methods that do not return a QuerySet should call ``_filter_visibility`` first."""
        attr = object.__getattribute__(self, name)
        # These methods evaluate the queryset and therefore need to filter the
        # visiblity set.
        evaluation_methods = (
            '_fetch_all', 'count', 'exists', 'aggregate', 'update', '_update',
            'delete', 'undelete',
        )
        if hasattr(attr, '__call__') and name in evaluation_methods:
            def decorator(*args, **kwargs):
                self._filter_visibility()
                return attr(*args, **kwargs)
            return decorator
        return attr

Removing _fetch_all from evaluation_methods fixes the issue, but not sure if that breaks anything else.

# visiblity set.
evaluation_methods = (
'_fetch_all', 'count', 'exists', 'aggregate', 'update', '_update',
'delete', 'undelete',
Copy link
Contributor Author

@AndreasBackx AndreasBackx Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could first be added here? I might have overlooked something when _fetch_all gets called. I used _fetch_all because it gets called in a lot of functions, but I'd have to look at it again. I think it's trying to filter the visibility after you call first when it should filter it before.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try that right away @AndreasBackx

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried to add first evaluation_methods, but that didn't help :/

Copy link
Contributor

@glemmaPaul glemmaPaul Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow 🎉
Looked into the code of django.models.query and saw it hit _fetch_all, upon looking at the method it looks like this:

def _fetch_all(self):
        if self._result_cache is None:
            self._result_cache = list(self.iterator())
        if self._prefetch_related_lookups and not self._prefetch_done:
            self._prefetch_related_objects()

So, fetching all if cached just by the pervious iterator, otherwise prefetch related objects. Prefetching usually means that it's a sliced queryset (correct me if I'm wrong). Changing the evaluation_methods _fetch_all to _prefetch_related_objects worked for me.

Copy link
Contributor

@glemmaPaul glemmaPaul Apr 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait that didn't do it, but there is something else that happened. Our UserManager is extending get_queryset to add some prefetch_related and select_related, removing that together with removing _fetch_all replacing it with _prefetch_related_objects works.

Is there a way around that issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please create a PR with a test case that clearly shows your problem. We can work on the PR and fix the problem without breaking any of the other tests.

@AndreasBackx
Copy link
Contributor Author

AndreasBackx commented Apr 26, 2017

@glemmaPaul could you perhaps make a PR with a test that fails?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants