Django | Adding/changing code in 3rd party packages with monkey patching - python

(+/- 3 months experience with Django)
I'm using the django-newsletter package to create and send newsletters to subscribers on my website. It uses a message.html template for which the context is added in the Submission model:
class Submission(models.Model):
# Some irrelevant code, only showing the send_message method below:
def send_message(self, subscription):
variable_dict = {
'subscription': subscription,
'site': Site.objects.get_current(),
'submission': self,
'message': self.message,
'newsletter': self.newsletter,
'date': self.publish_date,
'STATIC_URL': settings.STATIC_URL,
'MEDIA_URL': settings.MEDIA_URL
}
unescaped_context = get_context(variable_dict, autoescape=False)
subject = self.message.subject_template.render(
unescaped_context).strip()
text = self.message.text_template.render(unescaped_context)
message = EmailMultiAlternatives(
subject, text,
from_email=self.newsletter.get_sender(),
to=[subscription.get_recipient()],
headers=self.extra_headers,
)
if self.message.html_template:
escaped_context = get_context(variable_dict)
message.attach_alternative(
self.message.html_template.render(escaped_context),
"text/html"
)
# some more code irrelevant to the question
message.send()
Essentially upon Submitting the newsletter, the context is added in variable_dict in the Submission.send_message method.
In order to add more Personalization possibilities I would like to add my own context variables to this. From what I understand is that it's best to use monkey patching to achieve this, but I am not able to access the variable_dict from outside the method itself. Below is my monkey patch code:
## Monkey Patching ##
# monkey_patches.py
DJANGO_NEWSLETTER_CONTEXT_PATCH = True # adds context to Submission
def monkey_patch():
if DJANGO_NEWSLETTER_CONTEXT_PATCH:
from newsletter.models import Submission
old_send_message = Submission.send_message
def new_send_message(self, *k, **kw):
old_send_message(self, *k, **kw)
# trying to append to variable dict here, but failing:
print(variable_dict) # gives error
# self.send_message.variable_dict['test'] = 'testvalue' # gives error
# variable_dict['test'] = 'testvalue' # gives error
Submission.send_message = new_send_message
# apps.py
from django.apps import AppConfig
class SomeAppConfig(AppConfig):
name = 'SomeApp'
def ready(self):
try:
from SomeApp.monkey_patches import monkey_patch
monkey_patch()
except ImportError:
pass
Would really appreciate some help. Any pointers are welcome.

Related

Moving object id parameter to consumers.py Django-Channels

I have a problem. The point is that I am making application with Django backend and React frontend. I wanted to make a websocket which allows to write in live-chat rooms. The problem is that I have no idea how to load dynamic a Room id. Ill try to explain. The point is that connect method from ChatConsumer class will load messages realted to room and send it by json to frontend.
Ths is how it looks.
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_group_name = 'test'
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
messages = Message.objects.filter(room=[HERE I NEED TO PUT ID OF ROOM])
data_ready_for_json =list( messages.values('room','body','user'))
self.accept()
self.send(text_data=json.dumps({
'type':'chat',
'message': data_ready_for_json
}))
Iam trying to send this id from my views, where I have RoomRetrieveView built by generics.RetrieveAPIView.
Here it is:
class RoomRetrieveView(generics.RetrieveAPIView):
queryset = Room.objects.all()
permission_classes = (AllowAny,)
serializer_class = RoomSerializer
def get_serializer_context(self):
context = super(RoomRetrieveView,self).get_serializer_context()
context.update({'id' : self.get_object().id})
roomId = context['id']
return roomId
I was trying to move this roomId variable from get_serializer_context to my consumers.py file but it wants me to put "self" attribute but I have no idea how to figure it out. I also tried to use get_object method but it also not working. I have no idea. I also tried to use global to make variable global from method but its still not working. When Im trying to import anything from views.py file and do something I am getting
File "E:\Coding\Python\PORTFOLIO\Say-It-Social\venv\lib\site-packages\channels\routing.py", line 30, in get_default_application
raise ImproperlyConfigured("Cannot import ASGI_APPLICATION module %r" % path)
django.core.exceptions.ImproperlyConfigured: Cannot import ASGI_APPLICATION module 'sayitsocial.asgi'
You could use room id in the URL and retrive it using: self.room_name = self.scope['url_route']['kwargs']['id'] also avoid using a blocking code in consumers, use async ORM calls by writing the query in a separate method and use database_sync_to_async as a decorator you could import it from channels.db

Django Testing - loading data into the database before loading the apps

I'm currently writting some tests for a Django app (+ REST framework), and having some issues loading the test data into the database.
Let me explain with some (very simplified) code :
I have a django view which is something like :
view.py
from myapp.models import Item
from myapp.utils import MyClass
# need to initialize with the set of items
item_set = {item.name for item in Item.objects.all()}
my_class_object = MyClass(item_set)
class MyView(APIView):
def post(selfself, request):
result = my_class_object.process(request.data)
return Response(result)
So basically I need to initialize a class with some data from the database, and I then use this class in my view to process the data received by the endpoint.
Now the test :
my_test.py
from rest_framework.test import APILiveServerTestCase
from myapp.models import Item
class MyTest(APILiveServerTestCase):
def setUp(self):
self.URL = '/some_url_linking_to_myview/'
# load some data
Item.objects.create(name="first item")
Item.objects.create(name="second item")
def test_myview_return_correct_result(self):
post_data = {"foo"}
response = self.client.post(self.URL,
data=post_data,
format='json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, {"my_expected_result"})
When running the test, what currently happens is that view.py is loaded before the setUp() method get excecuted, so when I instantiate the class with these two lines :
item_set = {item.name for item in Item.objects.all()}
my_class_object = MyClass(item_set)
the database is still empty.
I'm wondering if there is a way to either get the data into the database before view.py get executed, or maybe somehow force reloading the app after setUp(), or instantiate my class somewhere else so it gets called after loading the data ?
thanks !
I think you are looking for setUpTestData().
Here is roughly how I set this up if I am going to use a significant amount of data:
tests.py
from django.test import TestCase
from .tests.test_data import base_data
class MyClassTest(TestCase):
#classmethod
def setUpTestData(cls):
base_data.base_data(cls)
base_data.py
from .models import MyClass
def base_data(cls):
cls.MyClass1 = MyClass.objects.create(
name="first_name"
)
cls.MyClass2 = MyClass.objects.create(
name="second_name"
)
Of course, you can do everything directly in the setUpTestData() function, if you would rather have your test data sitting up top.
Why not put the initiate code in the function and call from inside the post.
class MyView(APIView):
def initialize(self):
item_set = {item.name for item in Item.objects.all()}
my_class_object = MyClass(item_set)
def post(self, request):
self.initialize()
result = my_class_object.process(request.data)
return Response(result)
Edit 1
Optionally, you can use fixture to load MyClass objects in the database beforehand
class MyTest(APILiveServerTestCase):
fixtures = [
// my class objects fixtures file
]
def setUp():
// rest of the code

Class function is not found

I have the problem with creation of Referral from pinax-referrals package. Referral class has class function create(...) When I am trying to create referral inside view like:
from pinax.referrals.models import Referral
def createReferral(user):
referral = Referral.create(
user = user,
redirect_to = "/"
)
It throws me following error:
type object 'Referral' has no attribute 'create'
The code inside Pinax model looks ok:
#classmethod
def create(cls, redirect_to, user=None, label="", target=None):
if target:
obj, _ = cls.objects.get_or_create(
user=user,
redirect_to=redirect_to,
label=label,
target_content_type=ContentType.objects.get_for_model(target),
target_object_id=target.pk
)
else:
obj, _ = cls.objects.get_or_create(
user=user,
label=label,
redirect_to=redirect_to,
)
return obj
As I understand the problem is not connected to the Pinax package itself and looks really strange. Does anybody have any ideas?
It sounds like you have defined another class Referral inside the same module, that has replaced Pinax's Referral model.
This could happen because you have defined a class,
class Referral(View):
...
or maybe you have imported another class Referral. It might not be obvious this has happened if you do a * import.
from mymodule import *
A useful tool to debug is to add print(Referral) to you view. Then you will see whether the Referral class is the one you expect.

How to use Pyramid i18n outside of views and templates?

Pyramid documentation shows us how to use i18n inside views (and templates as well). But how to does one use it outside of views and templates where we have no access to current request (for example, in forms and models)?
#Michael said to pass request to models and forms. But is it right? I mean if form fields defines before __init__() method calls, the same with models. They don't see any parameters from views...
In Pylons we could simply use get_lang() and set_lang() and define preferable language in parent controller and then use ugettext() and ungettext() in any place we want without calling it from request directly every possible time (in views).
How to do that in Pyramid? Note that the language must be set from user's settings (session, cookies, db, etc).
My solution is to create the form class when it's needed with localizer as parameter. For example
forms.py
class FormFactory(object):
def __init__(self, localizer):
self.localizer = localizer
_ = self.localizer
self.required_msg = _(u'This field is required.')
self.invalid_email_msg = _(u'Invalid email address.')
self.password_not_match_msg = _(u'Password must match')
def make_contact_form(self):
_ = self.localizer
class ContactForm(Form):
email = TextField(_(u'Email address'), [
validators.Required(self.required_msg),
validators.Email(self.invalid_email_msg)
])
content = TextAreaField(_(u'Content'), [
validators.Required(self.required_msg)
])
return ContactForm
When you need to use the form
#view_config(route_name='front_pages.contact_us',
renderer='myweb:templates/front_pages/contact_us.genshi')
def contact_us(request):
"""Display contact us form or send mail
"""
_ = get_localizer(request)
factory = FormFactory(_)
ContactForm = factory.make_contact_form()
form = ContactForm(request.params)
return dict(form=form)
As you can see, we get the localizer in the view, and pass it to the FormFactory, then create a form with that factory. By doing that, all messages in the form was replaced with current locale language.
Likewise, you can do the same with model.
Have you found pyramid.18n.get_localizer yet?
http://docs.pylonsproject.org/projects/pyramid/en/1.3-branch/narr/i18n.html#using-a-localizer
Actually I had this very same problem. What I ended up doing was to see how the default locale negotiator works - it looks for a LOCALE property on the given request object. So just use a dummy to create the localizer. You may cache this value too, if you want
def my_get_localizer(locale=None):
request = Request({})
request._LOCALE_ = locale
return get_localizer(request)
Alternatively, join the irc channel #pyramid # freenode and pester the guys enough there to split the functionality of get_localizer in 2 separate documented functions (get_localizer and get_localizer_for_locale_name) for us to enjoy ;)
Also, notice that Pyramid TranslationStrings are lazy, so you can translate them as late as you want, e.g.
class MyModel(Base):
description = TranslationString("My model number ${number}")
...
def view(request):
m = MyModel()
localizer = get_localizer(request)
description = localizer.translate(m.description, mapping={'number': 1})
Sidenote: pylons' i18n was the worst can of worms I had opened in ages. The set_lang, get_lang hack was really awful, and pain in the ass as we needed to send emails to users in their native languages and then tried to restore the language back... also, it was IMPOSSIBLE to translate anything outside of a request in a pylons program, as a translator or the registry did not exist then.
You can make a localizer, and then translate a template accordingly.
When making the localizer, you can pass the lang you want (whether you have it from db or else). Hope it can help.
For the sake of clarity, I will set it as 'fr' below
from pyramid.i18n import make_localizer, TranslationStringFactory
from mako.template import Template
from mako.lookup import TemplateLookup
import os
absolute_path = os.path.dirname(os.path.realpath(__file__))
tsf = TranslationStringFactory('your_domain')
mako_lookup = TemplateLookup(directories=['/'])
template = Template(filename=template_path, lookup=mako_lookup)
localizer = make_localizer("fr", [absolute_path + '/../locale/'])
def auto_translate(*args, **kwargs):
return localizer.translate(tsf(*args, **kwargs))
# Pass _ pointer (translate function) to the context
_ = auto_translate
context.update({
"_": _
})
html = template.render(**context)
EDIT
You can also put this logic into a small function
def get_translator(lang):
"""
Useful when need to translate outside of queries (no pointer to request)
:param lang:
:return:
"""
localizer = make_localizer(lang, [absolute_path + '/../locale/'])
def auto_translate(*args, **kwargs):
return localizer.translate(tsf(*args, **kwargs))
_ = auto_translate
return _

werkzeug mapping urls to views (via endpoint)

Starting using werkzeug, i try to map urls (from a file urls.py) to views (from a folder views and then in different files to manage differents kinds of view), my folder organisation looks like that :
myapp/
application.py
urls.py
views/
__init__.py
common.py
places.py
...
my urls.py files looks like that:
from werkzeug.routing import Map, Rule
url_map = Map([
Rule('/places', endpoint='places.overview')
])
and obviously i got that piece in the views/places.py file :
def overview(request):
mycode...
render_template('places.html', extra...)
Most of werkzeug examples show the utilisation of the decorator expose to attach urls to views. It's practical for an app with 5 or 6 urls but can become a hell when you got more...
Is there a simple way to map the urls directly to the views???
Thanks.
Here is a simplified example:
import views
def app(environ, start_response):
urls = url_map.bind_to_environ(environ)
request = Request(environ)
endpoint, params = urls.match()
names = endpoint.split('.')
view = views
for name in names:
if not hasattr(view, name):
__import__(view.__name__, None, None, [name])
view = getattr(view, name)
try:
response = view(request)
except werkzeug.exceptions.HTTPException, exc:
response = exc
return response(environ, start_response)
import letters # our views module
url_map = Map([
Rule('/letters', endpoint=letters.index),
Rule('/letters/<int:item_id>', endpoint=letters.item),
Rule('/letters/<string:section_slug>', endpoint=letters.index),
Rule('/letters/<string:section_slug>/<int:item_id>',
endpoint=letters.item),
])
endpoint can be anything, including function, so you can just skip import magic from Denis's example
I'm not sure if it is preferred way to tackle this problem (I haven't found any similar example in werkzeug repo and I'm still only playing with this lib) but it is also possible to simply subclass Rule:
class CoolRule(Rule):
def __init__(self, view, *args, **kwargs):
self.view = view
super(CoolRule, self).__init__(*args, **kwargs)
def empty(self):
"""We need this method if we want to use
Submounts or Subdomain factories
"""
defaults = dict(self.defaults) if self.defaults else None
return CoolRule(self.view, self.rule, defaults, self.subdomain,
self.methods, self.build_only, self.endpoint,
self.strict_slashes, self.redirect_to,
self.alias, self.host)
_url_map = Map([
CoolRule(user.views.login, '/login', endpoint='user-login'),
CoolRule(user.views.logout, '/logout', endpoint='user-logout'),
])
def dispatch(request):
urls = _url_map.bind_to_environ(request.environ)
rule, arguments = urls.match(return_rule=True)
return rule.view(request, **arguments)
In that way you can preserve layer of view naming abstraction and avoid strange magic with 'string importing'.

Categories