For my current satchmo store, I would like to send html email instead of all the txt email. By the looks of the satchmo_store account registration code, all the emails are hardcoded and uses .txt format instead of html format.
e.g. mail.py
"""Sends mail related to accounts."""
from django.conf import settings
from django.utils.translation import ugettext
from satchmo_store.mail import send_store_mail
from satchmo_store.shop.models import Config
from satchmo_store.shop.signals import registration_sender
import logging
log = logging.getLogger('satchmo_store.accounts.mail')
# TODO add html email template
def send_welcome_email(email, first_name, last_name):
"""Send a store new account welcome mail to `email`."""
shop_config = Config.objects.get_current()
subject = ugettext("Welcome to %(shop_name)s")
c = {
'first_name': first_name,
'last_name': last_name,
'site_url': shop_config.site and shop_config.site.domain or 'localhost',
'login_url': settings.LOGIN_URL,
}
send_store_mail(subject, c, 'registration/welcome.txt', [email],
format_subject=True, sender=registration_sender)
I know you can change the last line to the following in order to make it work:
send_store_mail(
subject=subject,
context=c,
template='registration/welcome.txt',
recipients_list=[email],
format_subject=True,
sender=registration_sender,
template_html='registration/welcome.html')
However, it would be in my best interest not to touch the code in Satchmo app for the upgrade purpose in the near future.
Does anyone know what would be the ideal way to override this function or enable the html email for all the registration related functions without touching the satchmo app?
Thanks in advance.
I have done similar changes to Satchmo internals in the following way:
It should be possible to copy the relevant file from the Satchmo installation into your django application. If you set up your Satchmo shop according to this recommendation, that would likely mean copying satchmo/apps/satchmo_store/accounts/mail.py to /localsite/accounts/mail.py. The idea is to automatically load the local copy instead of the original.
In your local copy of mail.py you could then replace the send_store_email() function. Keep a note so that you will remember your changes when it comes to a Satchmo upgrade. Quite likely the original file will still be the same, and your override will work even with future versions.
In other cases when you have to change some class behaviour you can also subclass the original class with one changing only the relevant methods, while keeping the original name.
Related
I have a django server with an admin panel.
Different users make changes there and this is saved via auditlog in the database and displayed in the "history".
But there are situations when a user enters under the account of another user and makes changes on his behalf.
In order to identify from which device this or that change was made, it was a nice decision to also record data about the IP of the user from whom the change was made, and his unique device number.
By overloading several methods in the "AuditlogMiddleware" class, I got the desired result via "uuid.UUID(int=uuid.getnode())".
(Tested locally, because the prod server is heavily loaded and there is no way to do test committees)
from __future__ import unicode_literals
import threading
import time
from auditlog.middleware import AuditlogMiddleware
threadlocal = threading.local()
class ExtendedAuditlogMiddleware(AuditlogMiddleware):
def process_request(self, request):
threadlocal.auditlog = {
'signal_duid': (self.__class__, time.time()),
'remote_addr': request.META.get('REMOTE_ADDR'),
}
super(ExtendedAuditlogMiddleware, self).process_request(request)
**#changes here
import uuid
threadlocal.auditlog['additional_data'] = str(uuid.UUID(int=uuid.getnode()))+" | "+request.META["USERNAME"]**
# #staticmethod
def set_actor(self, user, sender, instance, signal_duid, **kwargs):
super(ExtendedAuditlogMiddleware, self).set_actor(user, sender, instance, signal_duid, **kwargs)
**#changes here
instance.additional_data = threadlocal.auditlog['additional_data']**
But the problem is that I think I get the UUID not of the user, but of the server, because there is no access to the user, i guess. I couldn't find the information, and I couldn't come up with my own solution either.
Question - is it even possible to get information from the server about django admin users' devices??
If not, what can i use instead of UUID to identify which device was used while making changes in django admin panel??
Thank you all in advance!
try to use javascript in side of your template to send this info thrugh
The current email that goes out to admins during errors contains sensitive information so I'd like to change course and instead include instructions on how to quickly view the log files and find the traceback. I'm not sure how to piggyback off the current functionality and add a custom message rather than the standard message. So any help would be awesome.
You will have to write your own handler for this.
Normally you would set it up as
"class": "django.utils.log.AdminEmailHandler",
Instead create your own class, inherit from AdminEmailHandler and overwrite the method
Quick, dirty, not tested:
from django.utils.log import AdminEmailHandler
class MyAdminEmailHandler(AdminEmailHandler):
def send_mail(self, subject, message, *args, **kwargs):
# Handle you sending an email
Now the class would
"class": "myapp.utils.log.MyAdminEmailHandler",
Example for user "Bob"
Django Admin: Home › Authentication and Authorization › Users › Bob
When modifying a user in Django Admin interface, under the password it says:
Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using this form.
However, the this form link fails with a Page not found 404, and the address is:
http://127.0.0.1:8000/password/
Whereas the correct URL (which works) should be:
http://127.0.0.1:8000/admin/auth/user/40/password/
After some searching I found: Lib/site-packages/django/contrib/auth/forms.py:129 :
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_(
"Raw passwords are not stored, so there is no way to see this "
"user's password, but you can change the password using "
"this form."
),
)
But I dont see how the change password link is set in the href, it looks blank to me. I also dont want to change the Django sourcecode because then the same bug will manifest when there are updates. How to I get the link to point to the change password page which works?
Are you using a custom user model or user admin? If that's the case, your admin class should be BaseUserAdmin or inherited from that.
You are not using this UserChangeForm. For custom user admin, follow this documentation Customizing Authentication
Turned out to be something very obscure. So after a lot of reverting and modifying new config files to work with old code, managed to narrow it done to a single line in my: templates\admin\base.html, the first line of the <head> section was:
<base href="/"><!-- For CSP stylesheet href links to pass MIME TYPE checks -->
The <base> tag:
specify a default URL and a default target for all links on a page
The link in the href seems to be set in the UserChangeForm's __init__ method:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password = self.fields.get('password')
if password:
password.help_text = password.help_text.format('../password/') # <-- here
# ...
I'm not sure why yours was taking you all the way back to localhost:8000/password/ as it's meant to go up one "directory" (from /admin/auth/user/40/change to /admin/auth/user/40) and then down to /admin/auth/user/40/password/.
The other implication of this href format is that if your URL structure doesn't have a trailing slash (eg /.../users/40/change), the ../password/ a. will take you to /.../users/password/ and you'll get a "No user with PK=password" error, and b. won't work with your URL structure anyway because it's added a trailing slash.
It does seem curious that Django doesn't have a way to override it (eg by passing the UserChangeForm a password_url or something). There might be a bug report / feature suggestion here...
As for actually dealing with it, one option, as suggested in this answer, is to manipulate your URL config to set the password change URL manually and point it to the relevant admin / auth view.
Unfortunately that doesn't help if you are subclassing or using the form elsewhere. In that case, the only solution I've thought of so far is to manually replace it in your subclass's __init__ like so:
from django.contrib.auth import forms as auth_forms
class UserEditForm(auth_forms.UserChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password = self.fields.get('password')
if password:
password.help_text = password.help_text.replace(
'../password/', kwargs.pop('password_url', './password'))
Unfortunately you have to use .replace as you can't reformat the help text once the super().__init__ has formatted it, and you can't do it before calling super().__init__ as you won't be able to access the field. This method does at least allow for passing a password_url as an argument to the form, and otherwise defaults to ./password which will work for URL structures without a trailing slash.
I want to render my website name in django templates. Django's own docs on Sites state:
Use it if your single Django installation powers more than one site
and you need to differentiate between those sites in some way.
My django app doesn't. I know I can still use it, but it seems like an overkill. I just want to pull a variable with my website's name (!= domain) in ANY template. I don't want to pass it in views either because that doesn't seem DRY enough.
Writing a custom processor seemed like a simple-enough option, but for some reason these variables aren't available in the .txt emails django-registration sends (while other variables definitely are, so I guess it's not impossible).
TIA
Edit: was asked to include code that doesn't work:
processors.py:
def get_website_name(request):
website_name = 'SomeWebsite'
return {'mysite_name': website_name}
Included successfully in context_processors in settings.py. It works nicely in "regular" templates, but not in emails.
Here's how I'm sending the emails, inside a change_email_view:
msg_plain = render_to_string('email_change_email.txt', context)
msg_html = render_to_string('email_change_email.html', context)
send_mail(
'Email change request',
msg_plain,
'my#email',
[profile.pending_email],
html_message=msg_html,
)
A further problem is that django-regitration further abstracts some of those views away: so when a user registers, wants to reset a password, etc...I don't even have access to the views.
Based on Django custom context_processors in render_to_string method you should pass the request to render_to_string.
msg_plain = render_to_string('email_change_email.txt', context, request=request)
msg_html = render_to_string('email_change_email.html', context, request=request)
I have a Django app. When logged in as an admin user, I want to be able to pass a secret parameter in the URL and have the whole site behave as if I were another user.
Let's say I have the URL /my-profile/ which shows the currently logged in user's profile. I want to be able to do something like /my-profile/?__user_id=123 and have the underlying view believe that I am actually the user with ID 123 (thus render that user's profile).
Why do I want that?
Simply because it's much easier to reproduce certain bugs that only appear in a single user's account.
My questions:
What would be the easiest way to implement something like this?
Is there any security concern I should have in mind when doing this? Note that I (obviously) only want to have this feature for admin users, and our admin users have full access to the source code, database, etc. anyway, so it's not really a "backdoor"; it just makes it easier to access a user's account.
I don't have enough reputation to edit or reply yet (I think), but I found that although ionaut's solution worked in simple cases, a more robust solution for me was to use a session variable. That way, even AJAX requests are served correctly without modifying the request URL to include a GET impersonation parameter.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
Usage:
log in: http://localhost/?__impersonate=[USERID]
log out (back to admin): http://localhost/?__unimpersonate=True
It looks like quite a few other people have had this problem and have written re-usable apps to do this and at least some are listed on the django packages page for user switching. The most active at time of writing appear to be:
django-hijack puts a "hijack" button in the user list in the admin, along with a bit at the top of page for while you've hijacked an account.
impostor means you can login with username "me as other" and your own password
django-impersonate sets up URLs to start impersonating a user, stop, search etc
I solved this with a simple middleware. It also handles redirects (that is, the GET parameter is preserved during a redirect). Here it is:
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.user = models.User.objects.get(id=int(request.GET["__impersonate"]))
def process_response(self, request, response):
if request.user.is_superuser and "__impersonate" in request.GET:
if isinstance(response, http.HttpResponseRedirect):
location = response["Location"]
if "?" in location:
location += "&"
else:
location += "?"
location += "__impersonate=%s" % request.GET["__impersonate"]
response["Location"] = location
return response
#Charles Offenbacher's answer is great for impersonating users who are not being authenticated via tokens. However, it will not work with clients side apps that use token authentication. To get user impersonation to work with apps using tokens, one has to directly set the HTTP_AUTHORIZATION header in the Impersonate Middleware. My answer basically plagiarizes Charles's answer and adds lines for manually setting said header.
class ImpersonateMiddleware(object):
def process_request(self, request):
if request.user.is_superuser and "__impersonate" in request.GET:
request.session['impersonate_id'] = int(request.GET["__impersonate"])
elif "__unimpersonate" in request.GET:
del request.session['impersonate_id']
if request.user.is_superuser and 'impersonate_id' in request.session:
request.user = User.objects.get(id=request.session['impersonate_id'])
# retrieve user's token
token = Token.objects.get(user=request.user)
# manually set authorization header to user's token as it will be set to that of the admin's (assuming the admin has one, of course)
request.META['HTTP_AUTHORIZATION'] = 'Token {0}'.format(token.key)
i don't see how that is a security hole any more than using su - someuser as root on a a unix machine. root or an django-admin with root/admin access to the database can fake anything if he/she wants to. the risk is only in the django-admin account being cracked at which point the cracker could hide tracks by becoming another user and then faking actions as the user.
yes, it may be called a backdoor, but as ibz says, admins have access to the database anyways. being able to make changes to the database in that light is also a backdoor.
Set up so you have two different host names to the same server. If you are doing it locally, you can connect with 127.0.0.1, or localhost, for example. Your browser will see this as three different sites, and you can be logged in with different users. The same works for your site.
So in addition to www.mysite.com you can set up test.mysite.com, and log in with the user there. I often set up sites (with Plone) so I have both www.mysite.com and admin.mysite.com, and only allow access to the admin pages from there, meaning I can log in to the normal site with the username that has the problems.