I have the following (simplified) Model:
class Order(models.Model):
is_anonymized = models.BooleanField(default=False)
billing_address = models.ForeignKey('order.BillingAddress', null=True, blank=True)
I want to hide the billing_address for objects where the customer chosen to do so by setting is_anonymized=True.
My best approach so far was to do that in the init:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.is_anonymized:
self.billing_address = None
self.billing_address_id = None
This works fine in the Admin BUT...
anywhere in the code there are select_related-QuerySets for Orders:
queryset = Order._default_manager.select_related('billing_address')
All places where the select_related-querysets are used, the billing_address is accidentally shown.
Elsewhere (like in the admin), it isn't.
How can I ensure to remove the billing_address everywhere for objects with is_anonymized = True?
I thought about overwriting the queryset in the manager but i couldn't overwrite the billing_address field by condition.
Using the getter-setter pattern was not a good solution because it breaks the admin at multiple places (there are many attributes to cloak like billing_address).
To be more precise:
The goal is to only simulate as if the data would be deleted although persisting in the database.
I would like to start by saying that I do not understand why would you want to hide information from the admin of your system. Unless you have a complex work environment where only the DBA have access to such information, I honestly do not see the point.
To answer your question...
To hide information in the admin page, one option is to disable all links and replace the HTML with the edit link when is_anonymized value is False:
(adapted from answer_1 and answer_2)
admin.py:
from django.utils.html import format_html
class OrderAdmin(admin.ModelAdmin):
list_display = ['anonymous_address']
def anonymous_address(self, obj):
if not obj.is_anonymized:
return format_html(u'{}', obj.id, obj.billing_address.address)
else:
return ("%s" % ('anonymous'))
def __init__(self, *args, **kwargs):
super(OrderAdmin, self).__init__(*args, **kwargs)
self.list_display_links = None
admin.site.register(Order, OrderAdmin)
Note that with this solution admin still has access to BillingAddress model, if you registered it in the admin site. In that case it will be also necessary to override that.
On your queries, you can aggregate values with conditional expressions:
views.py:
from core.models import Order
from django.db.models import When, Case
def anonymous_address(request):
orders = Order.objects.annotate(anonymised_address=Case(
When(is_anonymized=True, then=None),
When(is_anonymized=False, then='billing_address'),
)).values('is_anonymized', 'anonymised_address')
context = {'orders': orders}
return render(request, 'anonymous_address.html', context)
anonymous_address.html:
{% block content %}
{% for order in orders %}
Should be anonymous: {{order.is_anonymized}} <br>
Address: {{order.anonymised_address}}
<hr>
{% endfor %}
{% endblock content %}
And, instead of having this long query in every view, it is possible to replace that by a custom manager:
models.py:
class AnonymousOrdersManager(models.Manager):
def get_queryset(self):
return super().get_queryset().annotate(anonymised_address=Case(
When(is_anonymized=True, then=None),
When(is_anonymized=False, then='billing_address'),
)).values('is_anonymized', 'anonymised_address')
class Order(models.Model):
is_anonymized = models.BooleanField(default=False)
billing_address = models.ForeignKey(BillingAdress, null=True, blank=True, on_delete=models.CASCADE)
objects = models.Manager()
anonymous_orders = AnonymousOrdersManager()
views.py:
def anonymous_address(request):
orders = Order.anonymous_orders.all()
context = {'orders': orders}
return render(request, 'anonymous_address.html', context)
Related
I am trying to allow users to save details of a workout for a specific exercise through submitting a form. My ExerciseDetailView displays the form how I'd like it to:
class ExerciseDetailView(DetailView):
model = Exercise
template_name = 'workouts/types.html'
def get_context_data(self, **kwargs):
context = super(ExerciseDetailView, self).get_context_data(**kwargs)
context['form'] = WorkoutModelForm
return context
But my problem is with saving the inputted data in the database. I have tried making both a FormView and a CreateView but am clearly missing something:
class ExerciseFormView(FormView):
form_class = WorkoutModelForm
success_url = 'workouts:exercise_detail'
def form_valid(self, form):
form.save()
return super(ExerciseFormView, self).form_valid(form)
Here is my referenced WorkoutModelForm:
class WorkoutModelForm(forms.ModelForm):
class Meta:
model = Workout
fields = ['weight', 'reps']
My template:
<form action="{% url 'workouts:workout' exercise.id %}" method="post">
{% csrf_token %}
{{ form }}
<button type="submit">Save</button>
</form>
Urls:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/detail/', ExerciseFormView.as_view(), name='workout'),
And for context here is my Workout model which contains a get_absolute_url method:
class Workout(models.Model):
weight = models.FloatField(default=0)
reps = models.PositiveIntegerField(default=0)
created = models.DateField(auto_now_add=True)
updated = models.DateField(auto_now=True)
exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, default=None)
def get_absolute_url(self):
return reverse('exercise_detail', args=[str(self.pk)])
I am not receiving any errors, but when I submit the form my url remains the same, as I hoped, however the page just appears blank and the objects are not recorded. Can anybody please help me see what the problem is?
The problem is not your view, the Django logic will never trigger this view, the URLs are perfectly overlapping, so that means that for a URL, it will always trigger the first view (here the ExerciseDetailView), you should make the paths non-overlapping, for example with:
path('exercise/<int:pk>/detail/', ExerciseDetailView.as_view(), name='exercise_detail'),
path('exercise/<int:pk>/workout/', ExerciseFormView.as_view(), name='workout'),
Triggering the logic will however not be sufficient, since it will not link the Workout to the necessary exercise, you can alter the logic to:
from django.urls import reverse
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
def form_valid(self, form):
form.instance.exercise_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self):
return reverse('workouts:exercise_detail', kwargs={'pk': self.kwargs['pk']})
Need use CreateView
from django.views.generic.edit import CreateView
class ExerciseFormView(CreateView):
form_class = WorkoutModelForm
...
Error with a slug in Django - template it shows all posts data in each post
when I create a new post and write my data it shows all data from other posts why is that?
and how I can fix it?
also how I can add an auto-generation slug?
models.py :
from django.urls import reverse
from django.utils.text import slugify
class Android(models.Model):
title = models.CharField(max_length=50,default="",help_text="this is title for slug not post!")
name = models.CharField(max_length=50,default="")
app_contect = models.CharField(max_length=240,default="")
app_image = models.ImageField(upload_to='images/',null=True, blank=True)
post_date = models.DateTimeField(auto_now_add=True, null=True, blank=True)
post_tag = models.CharField(max_length=50,default="",choices = BLOG_SECTION_CHOICES)
slug = models.SlugField(null=True,uniqe=True) # new
def get_absolute_url(self):
return reverse('android_posts', kwargs={'slug': self.slug}) # new
def get_image(self):
if self.app_image and hasattr(self.app_image, 'url'):
return self.app_image.url
else:
return '/path/to/default/image'
def __str__(self):
return self.name
class Meta:
ordering = ('-post_date',)
views.py :
def android_posts(request,slug):
android_posts = Android.objects.all()
context = {'android_posts':android_posts}
return render(request,'android/android_problems_fix.html', { 'android_posts': android_posts })
html page :
{% for android in android_posts %}
<h1 id="font_control_for_header_in_all_pages">{{android.name}}</h1>
<hr>
<p id="font_control_for_all_pages">{{android.app_contect}}</p>
{% endfor %}
url :
path('Android/<slug:slug>', views.android_posts, name='android_posts'),
To autogenerate your slug (and only do it on initial save, so that it remains consistent), add the generation to your model save method:
def save(self, *args, **kwargs):
super(<Model>, self).save(*args, **kwargs)
if not self.pk:
self.slug = <slugify code here>
As for your view/Template, you are specifically selecting all posts using:
android_posts = Android.objects.all()
Passing them to the template, then looping over them with the for loop to display them all.
Instead of this, select only a single object with:
android_post = Android.object.get(pk=<pk value>)
Edit after you added your urls.py:
You can get the unique object for a slug with:
android_post = get_object_or_404(Android, slug=slug)
The use of get_object_or_404 will also handle the case where that record doesn't exist.
You haven't posted your urls.py, so not sure how you're linking to this view, but if it includes the slug in the url, you will be able to get this in the view. My guess is you're not accessing via slug in the url, but via the id field.
Personally, when I slugify some text, I always include the id - it is a better way of ensuring uniqueness. By specifying unique=True on your non-pk slug field, you are likely restricting the titles people can use (2 people couldn't use the same title then!)
To give you an example, this is how I am doing it on one of my models:
def save(self, *args, **kwargs):
if not self.id or not self.slug:
super(Android, self).save(*args, **kwargs)
self.slug = slugify(f"{self.title} {str(self.id)}")
super(Android, self).save(*args, **kwargs)
This slug will always be unique because it includes id - and 2 people could have a record with the same title value without the system objecting.
I've been stuck in this for several weeks now and I believe the answer is super simple but somehow I can't find it anywhere online. Which makes me think I'm going about it totally wrong.
All I want to do is be able to filter my stats such as the get_largest_winning_trade function based on the django-filter package. Where am I going wrong? As a side note get_largest_winning_trade is showing the largest winning trade in the Trade Model but it is not being filtered for my criteria. Such as "user".
managers.py
from django.db import models
class TradeQuerySet(models.QuerySet):
def get_users_trades(self, username):
return self.filter(user__username=username)
class TradeManager(models.Manager):
def get_queryset(self):
return TradeQuerySet(self.model, using=self._db)
def get_users_trades(self, username):
return self.get_queryset().get_users_trades(username)
def get_largest_winning_trade(self):
return max([t.profit_loss_value_fees for t in self.all()])
views.py
class StatsView1(LoginRequiredMixin, ListView):
model = Trade
template_name = 'dashboard/stats1.html'
def get_context_data(self, **kwargs):
filter = StatsFilter1(self.request.GET, queryset=self.get_queryset())
context = super().get_context_data(**kwargs)
context['filter'] = filter
context['get_largest_winning_trade'] = Trade.objects.get_largest_winning_trade
return context
stats.html (testing)
filter.qs.get_largest_winning_trade: {{ filter.qs.get_largest_winning_trade }} <br>
Trade.get_largest_winning_trade: {{ Trade.get_largest_winning_trade }} <br>
trade.get_largest_winning_trade: {{ trade.get_largest_winning_trade }} <br>
get_largest_winning_trade: {{ get_largest_winning_trade }} <br> # works but not with filter
Additional Requested Information
Shared the class, it's quite long so I tried to reduce it to what is most helpful. Please let me know if there's anything else.
models.py
class Trade(models.Model):
class Meta:
verbose_name = "Trade"
verbose_name_plural = "Trades"
...
user = models.ForeignKey(User, on_delete=models.CASCADE, blank=True)
status = models.CharField(max_length=2, choices=STATUS_CHOICES, default='cl')
type = models.CharField(max_length=5, choices=TYPE_CHOICES, default=LONG)
broker = models.ForeignKey(Broker, on_delete=models.CASCADE, blank=True, null=True)
asset = models.ForeignKey(Asset, default=DEFAULT_ASSET_ID, on_delete=models.CASCADE, null=True)
#AUTOMATED FIELDS
profit_loss_value_fees = models.FloatField(null=True)
objects = TradeManager()
...
def save(self):
...
self.profit_loss_value_fees = self.get_profit_loss_value_fees()
return super(Trade, self).save()
...
Probably you can do something like this using qs property:
def get_context_data(self, **kwargs):
filter = StatsFilter1(self.request.GET, queryset=self.get_queryset())
context = super().get_context_data(**kwargs)
context['filter'] = filter
context['get_largest_winning_trade'] = filter.qs.get_largest_winning_trade
return context
Update
I think rather than doing it manager, you can do the calculation here with aggregation. Like this:
from django.db.models import Max
...
context['get_largest_winning_trade'] = filter.qs.aggregate(max_value=Max('profit_loss_value_fees'))['max_value']
Reason for using aggregation is to reduce DB hits, because your manager method will hit database multiple times during loop iteration.
So I'm attempting to create an automated "tick sheet" as a proof of concept for a proposal to automate a process at my workplace.
At work we receive orders from various Suppliers and need to manually tick off the orders that are received against a list of what is expected on a paper sheet each day.
I'm using the generic DetailView in order to have a separate page for each Retailer and have a model representing the Retailer with a ManyToMany relationship with the Suppliers. I also have an Order model to 'simulate' a connection to the actual WMS database (with the intention of modifying it after getting actual db read access if/when this is put into production.)
I need the expected orders (in the Suppliers ManyToMany relationship) to match against those in the 'real' Order model data, and return an answer as to whether it exists in the db (in a way that I can display it on a template).
After a few frantic attempts to solve it myself I'm a little stumped as to how to achieve this within the context of the DetailView, so I fear I am misunderstanding something...
edit: I should have mentioned I only need the 'supplier code' to match but also intend to have the program check for duplicate orders using the 'order reference' once I've figured this out, as without this functionality the whole thing becomes a bit redundant...
My models.py:
from django.db import models
from django.utils import timezone
class Order(models.Model):
''' To simulate connection to main stock db '''
retailer_code = models.CharField(max_length=4)
retailer_name = models.CharField(max_length=100)
supplier_code = models.CharField(max_length=4)
supplier_name = models.CharField(max_length=100)
order_reference = models.CharField(max_length=20)
despatch_date = models.DateTimeField(default=timezone.now)
def __str__(self):
return f"< {self.order_reference}', {self.supplier_name}, {self.retailer_name} >"
# -------------------------------------------------------------------------------------
class Retailer(models.Model):
retailer_code = models.CharField(max_length=4)
retailer_name = models.CharField(max_length=100)
suppliers = models.ManyToManyField('Supplier')
slug = models.SlugField(unique=True, null=True)
def get_supplier_values(self):
return [(suppliers.supplier_code + ' - ' + suppliers.supplier_name) for suppliers in self.suppliers.all()]
def save(self, *args, **kwargs):
self.slug = self.slug or slugify(self.retailer_code)
super().save(*args, **kwargs)
def __str__(self):
return f"< {self.retailer_code} - {self.retailer_name} >"
class Supplier(models.Model):
supplier_code = models.CharField(max_length=4)
supplier_name = models.CharField(max_length=100)
def __str__(self):
return f"< {self.supplier_code}, {self.supplier_name} >"
My views.py:
from django.shortcuts import render
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from .models import Retailer, Order
class RetailerListView(ListView):
model = Retailer
context_object_name = 'retailer_list'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Select Retailer'
return context
class RetailerDetailView(DetailView):
model = Retailer
slug_field = 'retailer_code'
slug_url_kwarg = 'retailer_code'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Order Checklist'
context['db_orders'] = Order.objects.filter(retailer_code=self.object.retailer_code)
return context
def do_order_checklist(self):
pass # WIP
Any help would be appreciated...
Probably you can use Exists to to annotate if the order is in DB. For example:
from django.db.models import Exists, OuterRef
...
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Order Checklist'
squery=self.object.suppliers.filter(supplier_code=OuterRef('supplier_code'))
context['db_orders'] = Order.objects.filter(
retailer_code=self.object.retailer_code
).annotate(
in_db=Exists(squery)
)
return context
Then show in template:
{% for item in db_orders %}
{% if item.in_db %}
// do something
{% else %}
// else
{% endif %}
{% endfor %}
I see, probably the answer you are looking for is this.
as you get the list of supplier_codes in each retailer. instance you already have the list.
retailers_supplier_codes = [1, 2, 3, ...]
matching_orders = Order.objects.filter(supplier_code__in = retailers_supplier_codes)
I have a model, say 'Article', with a field
published = models.BooleanField(default=True)
and a template with condition:
{% if user.is_staff %}
<li>form.published.label_tag</li>
<li>form.published</li>
{% else %}
<li>form.published.as_hidden</li>
{% endif %}
and I use class-based generic views to add and update for this model.
In this case it is still possible for regular non-staff and malicious user to replace the value of published field.
I think I have to move the condition to views level to prevent this issue, somithing like
class ArticleEdit(UpdateView):
model = Article
form_class = ArticleForm
def form_valid(self, form):
self.object = form.save(commit=False)
if self.request.user.is_staff:
''' How to let the staff change this value? '''
else:
''' How to set previous value? '''
self.object.save()
return HttpResponseRedirect(self.get_success_url())
assuming that I remove this hidden field from template.
I would consider defining two forms, one for staff and one for regular users. You can then override the get_form_class method to select the correct form. If you exclude the published field from the form for non-staff, then they won't be able to change the value.
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ('published',)
class ArticleStaffForm(ArticleForm)
class Meta:
model = Article
exclude = ()
class ArticleEdit(UpdateView):
...
def get_form_class(self):
if self.request.user.is_staff:
return ArticleStaffForm
else:
return ArticleForm
you can do something like:
class MyForm(forms.Form):
def __init__(self, user, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
if not user.is_staff:
del self.fields['published']
and then pass the request.user object to the form when initialising it.
WARNING: Untested pseudo code. But this should give you an idea.