Did anyone managed to set a default handler in django-fobi? What was the approach? I'd like to set the db_store handler as a default for all of the forms.
I've tried to set defaults in models but with no success.
There's no out-of-box solution for that.
However, you could do as follows:
Solution 1:
Using django signals, watch updates of the FormEntry model and add the db_handler plugin programmatically each time a form is saved (it it doesn't yet have it assigned).
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
#receiver(post_save)
def update_document(sender, **kwargs):
app_label = sender._meta.app_label
model_name = sender._meta.model_name.lower()
instance = kwargs['instance']
if app_label == 'fobi' and model_name == 'formentry':
from fobi.models import FormHandlerEntry
FormHandlerEntry.objects.get_or_create(
plugin_uid='db_store',
form_entry=instance
)
Solution 2:
You could also register a form callback (fobi has callbacks implemented for almost each stage of form submission process).
In your callback you would have to mimic the functionality of the db_store plugin (copy-paste mainly).
import datetime
import simplejson as json
from fobi.base import (
form_callback_registry,
FormCallback,
get_processed_form_data,
)
from fobi.constants import CALLBACK_FORM_VALID
from fobi.contrib.plugins.form_handlers.db_store.models import SavedFormDataEntry
class AutoDbStore(FormCallback):
stage = CALLBACK_FORM_VALID
def callback(self, form_entry, request, form):
form_element_entries = form_entry.formelemententry_set.all()
# Clean up the values, leave our content fields and empty values.
field_name_to_label_map, cleaned_data = get_processed_form_data(
form,
form_element_entries
)
for key, value in cleaned_data.items():
if isinstance(value, (datetime.datetime, datetime.date)):
cleaned_data[key] = value.isoformat() \
if hasattr(value, 'isoformat') \
else value
saved_form_data_entry = SavedFormDataEntry(
form_entry=form_entry,
user=request.user if request.user and request.user.pk else None,
form_data_headers=json.dumps(field_name_to_label_map),
saved_data=json.dumps(cleaned_data)
)
saved_form_data_entry.save()
form_callback_registry.register(AutoDbStore)
Related
I'm trying to add custom functionality to django router methods.
This is my router that exposes the standard methods on an user.
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = [BasePermission]
I'm validating the user using serializer validation methods.
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
MOBILE_ERROR = 'Mobile number should be 10 digits long and only contain numbers.'
EMAIL_ERROR = 'Incorrect email format'
USERNAME_ERROR = 'Username must be at least 6 characters long and contain only letters and numbers.'
class Meta:
model = User
fields = '__all__'
def validate_mobile(self, value):
regexp = re.compile(r'^[0-9]{10}$')
if regexp.search(value):
return value
raise serializers.ValidationError(self.MOBILE_ERROR)
def validate_email(self, value):
if validate_email(value):
return value
raise serializers.ValidationError(self.EMAIL_ERROR)
def validate_username(self, value):
regexp = re.compile(r'^[a-zA-Z0-9]{6,}$')
if regexp.search(value):
return value
raise serializers.ValidationError(self.USERNAME_ERROR)
And this is my route.
router = DefaultRouter(trailing_slash=False)
router.register(r'user', UserViewSet),
urlpatterns = router.urls
I want to add a method send_activation_code if the user is created successfully. How do I do this?
For such purpose you can use signals. Every time when your app creates new User instance - some action should be performed. In your case you should connect build-in signal post_save and your existed send_activation_code function
Example for your case:
yourapp/signals.py:
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=User)
def send_activation_code_signal(sender, instance, created, **kwargs):
if created:
send_activation_code(instance.phone_number)
Also, you need to import signals in your app config file
yourapp/app.py:
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class YourAppConfig(AppConfig):
name = 'yourproject.yourapp'
verbose_name = _('yourapp')
def ready(self):
import yourproject.yourapp.signals
yourapp/__init__.py:
default_app_config = 'yourproject.yourapp.apps.YourAppConfig'
If you dont need to send code every time User instance created - you can specify more statements, for example:
if created and instance.validated:
send_activation_code(instance.phone_number)
There are some more useful built-in signals in Django, check docs
Django signals docs: https://docs.djangoproject.com/en/3.0/ref/signals/
I have done the below post_save signal in my project.
from django.db.models.signals import post_save
from django.contrib.auth.models import User
# CORE - SIGNALS
# Core Signals will operate based on post
def after_save_handler_attr_audit_obj(sender, **kwargs):
print User.get_profile()
if hasattr(kwargs['instance'], 'audit_obj'):
if kwargs['created']:
kwargs['instance'].audit_obj.create(operation="INSERT", operation_by=**USER.ID**).save()
else:
kwargs['instance'].audit_obj.create(operation="UPDATE").save()
# Connect the handler with the post save signal - Django 1.2
post_save.connect(after_save_handler_attr_audit_obj, dispatch_uid="core.models.audit.new")
The operation_by column, I want to get the user_id and store it. Any idea how can do that?
Can't be done. The current user is only available via the request, which is not available when using purely model functionality. Access the user in the view somehow.
I was able to do it by inspecting the stack and looking for the view then looking at the local variables for the view to get the request. It feels like a bit of a hack, but it worked.
import inspect, os
#receiver(post_save, sender=MyModel)
def get_user_in_signal(sender, **kwargs):
for entry in reversed(inspect.stack()):
if os.path.dirname(__file__) + '/views.py' == entry[1]:
try:
user = entry[0].f_locals['request'].user
except:
user = None
break
if user:
# do stuff with the user variable
Ignacio is right. Django's model signals are intended to notify other system components about events associated with instances and their respected data, so I guess it's valid that you cannot, say, access request data from a model post_save signal, unless that request data was stored on or associated with the instance.
I guess there are lots of ways to handle it, ranging from worse to better, but I'd say this is a prime example for creating class-based/function-based generic views that will automatically handle this for you.
Have your views that inherit from CreateView, UpdateView or DeleteView additionally inherit from your AuditMixin class if they handle verbs that operate on models that need to be audited. The AuditMixin can then hook into the views that successfully create\update\delete objects and create an entry in the database.
Makes perfect sense, very clean, easily pluggable and gives birth to happy ponies. Flipside? You'll either have to be on the soon-to-be-released Django 1.3 release or you'll have to spend some time fiddlebending the function-based generic views and providing new ones for each auditing operation.
You can do that with the help of middleware. Create get_request.py in your app. Then
from threading import current_thread
from django.utils.deprecation import MiddlewareMixin
_requests = {}
def current_request():
return _requests.get(current_thread().ident, None)
class RequestMiddleware(MiddlewareMixin):
def process_request(self, request):
_requests[current_thread().ident] = request
def process_response(self, request, response):
# when response is ready, request should be flushed
_requests.pop(current_thread().ident, None)
return response
def process_exception(self, request, exception):
# if an exception has happened, request should be flushed too
_requests.pop(current_thread().ident, None)
Then add this middleware to your settings:
MIDDLEWARE = [
....
'<your_app>.get_request.RequestMiddleware',
]
Then add import to your signals:
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from <your_app>.get_request import current_request
# CORE - SIGNALS
# Core Signals will operate based on post
def after_save_handler_attr_audit_obj(sender, **kwargs):
print(Current User, current_request().user)
print User.get_profile()
if hasattr(kwargs['instance'], 'audit_obj'):
if kwargs['created']:
kwargs['instance'].audit_obj.create(operation="INSERT", operation_by=**USER.ID**).save()
else:
kwargs['instance'].audit_obj.create(operation="UPDATE").save()
# Connect the handler with the post save signal - Django 1.2
post_save.connect(after_save_handler_attr_audit_obj, dispatch_uid="core.models.audit.new")
Why not adding a middleware with something like this :
class RequestMiddleware(object):
thread_local = threading.local()
def process_request(self, request):
RequestMiddleware.thread_local.current_user = request.user
and later in your code (specially in a signal in that topic) :
thread_local = RequestMiddleware.thread_local
if hasattr(thread_local, 'current_user'):
user = thread_local.current_user
else:
user = None
For traceability add two attributes to your Model(created_by and updated_by), in "updated_by" save the last user who modified the record. Then in your signal you have the user:
models.py:
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
created_by = models. (max_length=100)
updated_by = models. (max_length=100)
views.py
p = Question.objects.get(pk=1)
p.question_text = 'some new text'
p.updated_by = request.user
p.save()
signals.py
#receiver(pre_save, sender=Question)
def do_something(sender, instance, **kwargs):
try:
obj = Question.objects.get(pk=instance.pk)
except sender.DoesNotExist:
pass
else:
if not obj.user == instance.user: # Field has changed
# do something
print('change: user, old=%s new=%s' % (obj.user, instance.user))
You could also use django-reversion for this purpose, e.g.
from reversion.signals import post_revision_commit
import reversion
#receiver(post_save)
def post_revision_commit(sender, **kwargs):
if reversion.is_active():
print(reversion.get_user())
Read more on their API https://django-reversion.readthedocs.io/en/stable/api.html#revision-api
You can do a small hack by overriding you model save() method and setting the user on the saved instance as additional parameter. To get the user I used get_current_authenticated_user() from django_currentuser.middleware.ThreadLocalUserMiddleware (see https://pypi.org/project/django-currentuser/).
In your models.py:
from django_currentuser.middleware import get_current_authenticated_user
class YourModel(models.Model):
...
...
def save(self, *args, **kwargs):
# Hack to pass the user to post save signal.
self.current_authenticated_user = get_current_authenticated_user()
super(YourModel, self).save(*args, **kwargs)
In your signals.py:
#receiver(post_save, sender=YourModel)
def your_model_saved(sender, instance, **kwargs):
user = getattr(instance, 'current_authenticated_user', None)
PS: Don't forget to add 'django_currentuser.middleware.ThreadLocalUserMiddleware' to your MIDDLEWARE_CLASSES.
I imagine you would have figured this out, but I had the same problem and I realised that all the instances I create had a reference to the user that creates them (which is what you are looking for)
it's possible i guess.
in models.py
class _M(models.Model):
user = models.ForeignKey(...)
in views.py
def _f(request):
_M.objects.create(user=request.user)
in signals.py
#receiver(post_save, sender=_M)
def _p(sender, instance, created, **kwargs):
user = instance.user
No ?
Request object can be obtained from frame record by inspecting.
import inspect
request = [
frame_record[0].f_locals["request"]
for frame_record in inspect.stack()
if frame_record[3] == "get_response"
][0]
def get_requested_user():
import inspect
for frame_record in inspect.stack():
if frame_record[3] == 'get_response':
request = frame_record[0].f_locals['request']
return request.user
else:
return None
context_processors.py
from django.core.cache import cache
def global_variables(request):
cache.set('user', request.user)
----------------------------------
in you model
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.core.cache import cache
from news.models import News
#receiver(pre_delete, sender=News)
def news_delete(sender, instance, **kwargs):
user = cache.get('user')
in settings.py
TEMPLATE_CONTEXT_PROCESSORS = (
'web.context_processors.global_variables',
)
I'm struggling with the following.
I'm trying to create a custom signal that will trigger when the current time will be equal to the value of my model's notify_on DateTimeField.
Something like this:
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def send_email(*args, **kwargs):
# send email
signals.when_its_time.connect(send_email, sender=User)
After I've read through all docs and I found no information on how to implement such a signal.
Any ideas?
UPDATE:
Less naive approach with ability to discard irrelevant tasks: https://stackoverflow.com/a/55337663/9631956
Ok, thanks to comments by #SergeyPugach I've done the following:
Added a post_save signal that calls a function that adds a task to the celery. apply_async let's you pass eta - estimated time of arrival which can accept DateTimeField directly, that's very convenient.
# models.py
from django.db.models import signals
from django.db import models
from .tasks import send_notification
class Notification(models.Model):
...
notify_on = models.DateTimeField()
def notification_post_save(instance, *args, **kwargs):
send_notification.apply_async((instance,), eta=instance.notify_on)
signals.post_save.connect(notification_post_save, sender=Notification)
And the actual task in the tasks.py
import logging
from user_api.celery import app
from django.core.mail import send_mail
from django.template.loader import render_to_string
#app.task
def send_notification(self, instance):
try:
mail_subject = 'Your notification.'
message = render_to_string('notify.html', {
'title': instance.title,
'content': instance.content
})
send_mail(mail_subject, message, recipient_list=[instance.user.email], from_email=None)
except instance.DoesNotExist:
logging.warning("Notification does not exist anymore")
I will not get into details of setting up celery, there's plenty of information out there.
Now I will try to figure out how to update the task after it's notification instance was updated, but that's a completely different story.
In django's documentation there is two interesting signals that may help you on this task: pre_save and post_save.
It depends on your needs, but let's say you want to check if your model's notify_on is equal to the current date after saving your model (actually after calling the save() or create() method). If it's your case you can do:
from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save
class Notification(models.Model):
...
# Every notification is related to a user
# It depends on your model, but i guess you're doing something similar
user = models.ForeignKey(User, related_name='notify', on_delete=models.DO_NOTHING)
notify_on = models.DateTimeField()
...
def send_email(self, *args, **kwargs):
"""A model method to send email notification"""
...
#receiver(post_save, sender=User)
def create_notification(sender, instance, created, **kwargs):
# check if the user instance is created
if created:
obj = Notification.objects.create(user=instance, date=datetime.now().date())
if obj.notify_on == datetime.now().date():
obj.send_email()
And you should know, that django signals won't work by they own only if there is an action that triggers them. What this mean is that Django signals won't loop over your model's instances and perform an operation, but django signals will trigger when your application is performing an action on the model connected to a signal.
Bonus: To perform a loop over your instances and process an action regulary you may need an asyncworker with a Queue database (mostly, Celery with Redis or RabbitMQ).
I am using Django-allauth for my login/signup related stuff, so when a user signs up(first time) into my site, I am redirecting him to /thanks/ page by defining below setting in settings.py file
LOGIN_REDIRECT_URL = '/thanks/'
But when the user tried to log in for the next time(if already registered) I should redirect him to '/dashboard/' URL
So tried to alter that with Django-allauth signals like below which is not working at all
#receiver(allauth.account.signals.user_logged_in)
def registered_user_login(sender, **kwargs):
instance = User.objects.get_by_natural_key(kwargs['user'])
print instance.last_login==instance.date_joined,"??????????????????????????????"
if not instance.last_login==instance.date_joined:
return HttpResponseRedirect(reverse('dashboard'))
So can anyone please let me know how to redirect a user to /dashboard/ for the normal login, am I doing anything wrong in the above signal code?
Edit
After some modification according to the below answer by pennersr, my AccountAdapter class looks like below
from allauth.account.adapter import DefaultAccountAdapter
# from django.contrib.auth.models import User
class AccountAdapter(DefaultAccountAdapter):
def get_login_redirect_url(self, request):
if request.user.last_login == request.user.date_joined:
return '/registration/success/'
else:
return '/dashboard/'
But still, it is redirecting the user to /dashboard/, my logic in determining the first time user is wrong?
In general, you should not try to put such logic in a signal handler. What if there are multiple handlers that want to steer in different directions?
Instead, do this:
# settings.py:
ACCOUNT_ADAPTER = 'project.users.allauth.AccountAdapter'
# project/users/allauth.py:
class AccountAdapter(DefaultAccountAdapter):
def get_login_redirect_url(self, request):
return '/some/url/'
The two datetimes last_login and date_joined will always be different, although it might only be a few milliseconds. This snippet works:
# settings.py:
ACCOUNT_ADAPTER = 'yourapp.adapter.AccountAdapter'
# yourapp/adapter.py:
from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
from django.shortcuts import resolve_url
from datetime import datetime, timedelta
class AccountAdapter(DefaultAccountAdapter):
def get_login_redirect_url(self, request):
threshold = 90 #seconds
assert request.user.is_authenticated()
if (request.user.last_login - request.user.date_joined).seconds < threshold:
url = '/registration/success'
else:
url = settings.LOGIN_REDIRECT_URL
return resolve_url(url)
One important remark to pennersr answer: AVOID using files named allauth.py as it will confuse Django and lead to import errors.
the answer here is very simple, you do not need any signals or overriding the DefaultAccountAdapter
in settings.py just add a signup redirect_url
ACCOUNT_SIGNUP_REDIRECT_URL = "/thanks/"
LOGIN_REDIRECT_URL = "/dashboard/"
You can simply define those two other signals using user_logged_in signal as base. A good place to put it is on a signals.py inside a accounts app, in case you have one, or in you core app. Just remember to import signals.py in you __init__.py.
from django.dispatch import receiver, Signal
pre_user_first_login = Signal(providing_args=['request', 'user'])
post_user_first_login = Signal(providing_args=['request', 'user'])
#receiver(user_logged_in)
def handle_user_login(sender, user, request, **kwargs):
first_login = user.last_login is None
if first_login:
pre_user_first_login.send(sender, user=user, request=request)
print 'user_logged_in'
if first_login:
post_user_first_login.send(sender, user=user, request=request)
#receiver(pre_user_first_login)
def handle_pre_user_first_login(sender, user, request, **kwargs):
print 'pre_user_first_login'
#receiver(post_user_first_login)
def handle_post_user_first_login(sender, user, request, **kwargs):
print 'post_user_first_login'
I want to send an email when a specific field is changed in a model. Is it possible? Here is what I am looking for. I have a profile model that includes a BooleanField that when the administrator selects to be true I want to send user an email. I know I could put it in a "def save(self):" but, that fires off an email anytime the model is changed and the field is true. Is there a way to have it only email if the field was changed from False to True?
save method is a perfectly good place for what you want to do:
def save(self):
if self.id:
old_foo = Foo.objects.get(pk=self.id)
if old_foo.YourBooleanField == False and self.YourBooleanField == True:
send_email()
super(Foo, self).save()
You can use django-model-changes to do this without an additional database lookup:
from django.db import models
from django.dispatch import receiver
from django_model_changes import ChangesMixin
class MyModel(ChangesMixin, models.Model):
flag = models.BooleanField()
#receiver(pre_save, sender=MyModel)
def send_email_if_flag_enabled(sender, instance, **kwargs):
if instance.previous_instance().flag == False and instance.flag == True:
# send email
Something like this could help and only sends an email when change from false to true
#models.py
from django.contrib.auth.models import User
from django.db.models import signals
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import pre_save
from django.conf import settings
from django.core.mail import send_mail
#signal used for is_active=False to is_active=True
#receiver(pre_save, sender=User, dispatch_uid='active')
def active(sender, instance, **kwargs):
if instance.is_active and User.objects.filter(pk=instance.pk, is_active=False).exists():
subject = 'Active account'
mesagge = '%s your account is now active' %(instance.username)
from_email = settings.EMAIL_HOST_USER
send_mail(subject, mesagge, from_email, [instance.email], fail_silently=False)
Use hook a function with your models post_save using django signals (http://docs.djangoproject.com/en/dev/ref/signals/#django.db.models.signals.post_save)
In that function use standard django mailing: http://docs.djangoproject.com/en/dev/topics/email/