I have a user profile model that is storing configurations for a number of third party API keys, and I'm trying to determine how to best go about dynamically generating forms based on the choice that the user makes. The app supports only a subset of services, so I'm using a CharField (+ CHOICES) to narrow down what the user is trying to submit a configuration for. The user can submit as many duplicates as they would like (3 sets of Cloud Service 1 keys, for example)
I have this model:
class ServiceIntegration(models.Model):
profile = models.ForeignKey(
Profile,
on_delete=models.CASCADE
)
CS1 = 'CS1'
CS2 = 'CS2'
SERVICE_CHOICES = (
(CS1, 'Cloud Service 1'),
(CS2, 'Cloud Service 2'),
)
alias = models.CharField(max_length=255)
service = models.CharField(
max_length=255,
choices=SERVICE_CHOICES,
default=CS1
)
config = JSONField()
In a form, the user has a dropdown whose QuerySet is set to this model's objects. When the user makes a selection, I'd like to reach out to an endpoint and dump some form HTML in a predetermined location. Presumably, I could have a form set up for each integration, and simply have a view that takes in the choice, looks up the form, and renders it back out (same for POSTing data to that endpoint).
What's my best bet to render forms (and accept data) dynamically based on a user's choice?
I ended up writing a BaseService class that each integration implementation inherits from, and a form that each implementation returns like:
from service_integrations.forms import CloudServiceOneForm
class BaseService(ABC):
def __init__(self, configuration):
self.configuration = configuration
super().__init__()
#abstractmethod
def get_form(self):
pass
#abstractmethod
def get_service_name(self):
pass
#abstractmethod
def check(self):
pass
#abstractmethod
def run(self):
pass
class CloudServiceOne(BaseService):
def get_service_name(self):
return 'Cloud Service One' # This should match the CharField Choice
def get_form(self):
return CloudServiceOneForm()
def check(self):
# Requirements for config go here
if self.configuration.get('secret_key') and self.configuration.get('access_key'):
return True
else:
return False
def run(self):
pass
As well as a utility that can be called from a view to pass the relevant form into a context:
from service_integrations.classes import BaseService
def get_service(service):
# Passes empty configurations to each subclass of BaseService and returns the handler based on the name.
for cls in BaseService.__subclasses__():
if cls({}).get_service_name() == service:
return cls
return None
Then you can pass the form into the context like:
service = get_service('Cloud Service One')
context = {
'form': service.get_form()
}
return render(request, 'some_template.html', context)
Related
I'm working on my Django SAAS app in which I want to allow the user to have some custom settings, like disable or enable certain filters. For that I'm using django-user-setttings combined with django-filters and simple forms with boolean fields:
class PropertyFilterSetting(forms.Form):
filter_by_loans = forms.BooleanField(required=False)
filter_by_tenants = forms.BooleanField(required=False)
The issue is that when trying to apply those settings, I keep running into serious spaghetti code:
views.py
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
print(get_user_setting('filter_by_tenants', request=self.request))
return PropertyFilterWithoutTenant if not get_user_setting('filter_by_tenants', request=self.request)['value'] else PropertyFilter
filter.py
class PropertyFilter(django_filter.FilterSet):
...
class PropertyFilterWithoutTenant(PropertyFilter):
...
and I'd have to do the same thing with the rest of the features. Is there any better way to implement this?
You can create methods in your User model, or a new class which acts as a store for all the methods. Each method will give you the relevant filterset class based on the value of corresponding user setting.
Something like:
class UserFilterset:
def __init__(self, request):
self.request = request
def get_property_filterset(self):
if not get_user_setting('filter_by_tenants', request=self.request)['value']:
return PropertyFilterWithoutTenant
return PropertyFilter
... # add more such methods for each user setting
Now you can use this method to get the relevant filterset class
class PropertyListView(LoginRequiredMixin, FilterView):
template_name = 'app/property_list.html'
context_object_name = 'properties'
def get_filterset_class(self):
return UserFilterset(self.request).get_property_filterset()
So even if in future you want to add some more logic, you can just update the relevant method, it would be cleaner and manageable.
I'm not sure how MVT stucture will exactly respond to this one but i use a custom generic class in REST structure to add custom filter fields in any viewset that i want
class ListAPIViewWithFilter(ListAPIView):
def get_kwargs_for_filtering(self):
filtering_kwargs = {}
if self.my_filter_fields is not []:
# iterate over the filter fields
for field in self.my_filter_fields:
# get the value of a field from request query parameter
field_value = self.request.query_params.get(field)
if field_value:
filtering_kwargs[field] = field_value
return filtering_kwargs
def get_queryset(self):
queryset = super(ListAPIViewWithFilter, self).get_queryset()
filtering_kwargs = self.get_kwargs_for_filtering()
if filtering_kwargs != {}:
# filter the queryset based on 'filtering_kwargs'
queryset = queryset.filter(**filtering_kwargs)
self.pagination_class = None
else:
return queryset
return queryset[:self.filter_results_number_limit]
changing origional get_queryset function in views.py should be the key to solving your problem (it works in django rest).
try checking what function gets the data then just identify the field wanted from it.
Friends,
I have a Django app and I want to add some basic tracking for all my views. (Much like a counter or something similar)
What I have so far is that I can track specific objects with mixins. So every time someone is clicking on an instance of my model (the DetailView) an entry is added to my database. I did this via the django content types.
Now, to do this I need a get method to actually get a specific object back.
But in my ListView I don't have that object.
How could I implement something similar for either urls or just my ListView? Is that even possible? I'd like to record a single entry stating that the list of my model has been accessed.
Here is what I have so far:
my views
class ListJobView(ObjectViewMixin, ListView):
model = Job
context_object_name = 'jobs'
template_name = 'list_jobs.html'
ordering = '-pub_date'
# paginate_by = 1
class DetailJobView(ObjectViewMixin, DetailView):
model = Job
template_name = 'detail_job.html'
queryset = Job.objects.all()
def get_object(self):
id = self.kwargs.get("id")
return get_object_or_404(Job, id=id)
my mixin
from .signals import object_viewed_signal
class ObjectViewMixin:
def dispatch(self, request, *args, **kwargs):
try:
instance = self.get_object()
except self.model.DoesNotExist:
instance = None
if instance is not None:
object_viewed_signal.send(instance.__class__, instance=instance, request=request)
return super(ObjectViewMixin, self).dispatch(request, *args, **kwargs)
my signal
from django.dispatch import Signal
object_viewed_signal = Signal(providing_args=['instance', 'request'])
here is the signal handler:
def object_viewed_receiver(sender, instance, request, *args, **kwargs):
new_viewed_object = ObjectViewed.objects.create(
user = request.user,
content_type = ContentType.objects.get_for_model(sender),
object_id = instance.id,
)
object_viewed_signal.connect(object_viewed_receiver)
If I should provide more code please let me know.
Any help is highly appreciated...
So this answer is without any guarantees to work. I implemented this solution at the time I was asking the question but I am not using it anymore since I removed this functionality from my django app. A reason for this was the heavy load. Because in this solution I create an object for every time someone visits the website and save it to the DB. This works fine but gets really heavy for the DB after a while.
First I created and new app and called it analytics. In the app I created model like this:
class UrlTime(models.Model):
associated_url = models.CharField(blank=True, null= True, max_length=250)
timestamp = models.DateTimeField(auto_now=True, blank=True)
def __str__(self):
return self.associated_url
I created a new module called middleware.py. A middleware basically executes code either before a view is called or after a view is called (dependent where you put the code). If you count URLs I think it's good to have it before a view is called:
middleware.py
from .models import UrlTime
class GetUrlMiddleware():
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# before view
# creates a counter object for URL if url doesn't exist. Else counts up.
if request.path.startswith('/admin/'): ##exclude what you don't want to count
pass
else:
# urltrack, _ = UrlTrack.objects.get_or_create(url=request.path)
# urltrack.counter = F('counter') + 1
# urltrack.save()
urltime = UrlTime.objects.create(associated_url=request.path)
urltime.save()
response = self.get_response(request)
# after view
return response
With this you should be able to count your URls every time someone visits the page. An object will be created in the DB. Then you only have to display it how you want it on a template. I did something like this:
class AnalyticsIndexView(StaffRequiredMixin, ListView):
template_name = 'analytics_list.html'
model = UrlTime
def get_context_data(self, **kwargs):
context = super(AnalyticsIndexView, self).get_context_data(**kwargs)
context['object_viewed_list'] = ObjectViewed.objects.all()
context['url_views_list'] = UrlTime.objects.all()
context['total_views'] = UrlTime.objects.all().count
counting = UrlTime.objects.all().values('associated_url').annotate(url_count=Count('associated_url'))
context['home_view'] = counting
context['start'] = UrlTime.objects.first()
return context
Then you just need to implement the template.... If you need that too let me know and I post it here too.
My flask app centers around modifying models based on SQLAlchemy. Hence, I find flask-admin a great plugin because it maps my SQLA models to forms with views already defined with a customizable interface that is tried and tested.
I understand that Flask-admin is intended to be a plugin for administrators managing their site's data. However, I don't see why I can't use FA as a framework for my users to CRUD their data as well.
To do this, I have written the following:
class AuthorizationRequiredView(BaseView):
def get_roles(self):
raise NotImplemented("Override AuthorizationRequiredView.get_roles not set.")
def is_accessible(self):
if not is_authenticated():
return False
if not current_user.has_role(*self.get_roles()):
return False
return True
def inaccessible_callback(self, name, **kwargs):
if not is_authenticated():
return current_app.user_manager.unauthenticated_view_function()
if not current_user.has_role(*self.get_roles()):
return current_app.user_manager.unauthorized_view_function()
class InstructionModelView(DefaultModelView, AuthorizationRequiredView):
def get_roles(self):
return ["admin", "member"]
def get_query(self):
"""Jails the user to only see their instructions.
"""
base = super(InstructionModelView, self).get_query()
if current_user.has_role('admin'):
return base
else:
return base.filter(Instruction.user_id == current_user.id)
#expose('/edit/', methods=('GET', 'POST'))
def edit_view(self):
if not current_user.has_role('admin'):
instruction_id = request.args.get('id', None)
if instruction_id:
m = self.get_one(instruction_id)
if m.user_id != current_user.id:
return current_app.user_manager.unauthorized_view_function()
return super(InstructionModelView, self).edit_view()
#expose('/delete/', methods=('POST',))
def delete_view(self):
return_url = get_redirect_target() or self.get_url('.index_view')
if not self.can_delete:
return redirect(return_url)
form = self.delete_form()
if self.validate_form(form):
# id is InputRequired()
id = form.id.data
model = self.get_one(id)
if model is None:
flash(gettext('Record does not exist.'), 'error')
return redirect(return_url)
# message is flashed from within delete_model if it fails
if self.delete_model(model):
if not current_user.has_role('admin') \
and model.user_id != current_user.id:
# Denial: NOT admin AND NOT user_id match
return current_app.user_manager.unauthorized_view_function()
flash(gettext('Record was successfully deleted.'), 'success')
return redirect(return_url)
else:
flash_errors(form, message='Failed to delete record. %(error)s')
return redirect(return_url)
Note: I am using Flask-User which is built on top of Flask-Login.
The code above works. However, it is difficult to abstract as a base class for other models which I would like to implement access control for CRUD operations and Index/Edit/Detail/Delete views.
Mainly, the problems are:
the API method, is_accessible, does not provide the primary key of the model. This key is needed because in almost all cases relationships between users and entities are almost always stored via relationships or in the model table directly (i.e. having user_id in your model table).
some views, such as delete_view, do not provide the instance id that can be retrieve easily. In delete_view, I had to copy the entire function just to add one extra line to check if it belongs to the right user.
Surely someone has thought about these problems.
How should I go about rewriting this to something that is more DRY and maintainable?
I am working in Django and I have a situation where I have written a custom validator that lives in the model.py
This validator should return a validationError when the input is bad.
In the project I am working on, we are using Django Rest Framework for our API and the Django admin panel for our admin panel. They connect to the same DB
My problem is that when the request comes from the API I need to return a 'serializers.ValidationError' (which contains a status code of 400), but when the request comes from the admin panel I want to return a 'django.core.exceptions.ValidationError' which works on the admin panel. The exceptions.ValidationError does not display correctly in the API and the serializers.ValidationError causes the admin panel to break. Is there some way I can send the appropriate ValidationError to the appropriate place?
here is my validation function (it lives in the model)
def validate_unique(self, *args, **kwargs):
super(OrganizationBase, self).validate_unique(*args, **kwargs)
qs = self.__class__._default_manager.filter(organization_type="MEMBER")
if not self._state.adding and self.pk is not None:
qs = qs.exclude(pk=self.pk)
if qs.exists():
raise serializers.ValidationError("Only one organization with \'Organization Type\' of \'Member\' is allowed.") #api
raise exceptions.ValidationError("Only one organization with \'Organization Type\' of \'Member\' is allowed.") #admin
Those two lines at the end are the two errors written together for illustration's sake, in this case only the #api one would run
Basically I want to send errorA when the request is coming from the admin panel and errorB when the request is coming from the API
Thank you
For raising different error classes write different validators.
rest framework api:
You can use the UniqueValidator or a custom validation function. check link [1]
eg:
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = (....)
def validate(self, data):
# my validation code
raise serializers.ValidationError(....)
return data
admin panel:
for the admin panel you can use a custom form [2].
eg:
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def clean(self):
cleaned_data = super(MyForm, self).clean()
# my validation code
raise exceptions.ValidationError(....)
return cleaned_data
class MyAdmin(admin.ModelAdmin):
form = MyForm
In both the serializer and form you can access the instance object if not none.
[1] http://www.django-rest-framework.org/api-guide/validators/#uniquevalidator
[2] https://docs.djangoproject.com/en/1.11/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form
newbies to django1.6
i want to set cookie in class based generic view (Listview)
models.py
class Designation(Models.model):
title = models.CharField(max_length=50)
description = models.CharField(max_length=10000, blank=True)
views.py
class DesignationList(ListVew):
def get_queryset(self):
"""
will get 'sort_by' parameter from request,
based on that objects list is return to template
"""
col_nm = self.request.GET.get('sort_by', None)
if col_nm:
if cookie['sort_on'] == col_nm:
objects=Designation.objects.all().order_by(col_nm).reverse()
else:
cookie['sort_on'] = col_nm
objects=Designation.objects.all().order_by(col_nm)
else:
objects = Designation.objects.all().order_by('title')
//set cookie['sort_on']='title'
return objects
template
in template im iterating over objects
so initially objects display in sort_by 'title' desc.
"this values is i want to set in cookie".
in template, if user click over title,it will check in cookie
cookie['sort_on']='title'
then all objects are in asce order
if user click over description,then cookie value is replaced
cookie['sort_on']='description' and objects are in desc order..
soo,how to set cookie which i can use in whole ListView class.. ?
Thnx in advance..
In order to set/delete cookies you have to have access to the "response" object. To do so, in a class-based view, you can override "render_to_response".
Example:
class DesignationList(ListVew):
def render_to_response(self, context, **response_kwargs):
response = super(LoginView, self).render_to_response(context, **response_kwargs)
response.set_cookie('sort_on', 'title')
return response
Unless you have a very good reason, you shouldn't be using cookies, but the session framework. You can access that inside your methods with self.request.session, and it acts like a dictionary.
if col_nm:
if self.request.session.get('sort_on') == col_nm:
objects=Designation.objects.all().order_by(col_nm).reverse()
else:
self.request.session['sort_on'] = col_nm
objects=Designation.objects.all().order_by(col_nm)
etc.