Django sites framework country separation - django

I'm developing a web site with Django where we list products and some comments for them. I'd like to have separate sites for each country with different product objects. For example:
us.domain.com - products in U.S are listed and the page is English.
fr.domain.com - products in France are listed and the page is in French etc.
1) What came to my mind is to alter settings.SITE_ID depending on the subdomain requested in a middleware, but this is said to be dangerous. What else can I do? I don't want to run separate instances or settings.py file for each country.
My solution so far:
class SubdomainMiddleware:
def process_request(self, request):
host = request.META.get('HTTP_HOST', '')
try:
site = Site.objects.get(domain=host)
except:
site = Site.objects.get(domain='www.domain.com')
request.site = site
products = Product.objects.filter(site_id=request.site)
Here, below doesn't work because CurrentSiteManager uses settings.SITE_ID, not the request.site:
on_site = CurrentSiteManager()
Product.on_site.all()
I have to explicitly pass site_id=request.site at each query. Is this safe, or are there any other alternatives?
2) If I add site = models.ForeignKey(Site) field to the Product model, is it enough? Or should I add that field to all other models like Comment, etc.?

Related

What is the best practice to let users set a variable which is then be available for all views?

The app I'm working on is a kind of an internal portal for the big Holding (Group of Companies), where various financial and non-financial data stored for all the companies of the Holding. Users are logging into the portal and on the home page they see the list of all Companies that they are authorized to see.
Then user should be able to enter into one particular Company from the list and afterwards all subsequent views should show data filtered only for that selected Company until user does not exit it and enter into another Company from home page.
I'm trying to achieve this functionality by following but I'm not sure is it proper django way to do it:
In views.py I have Mylogin view which is setting global variable Selected_Company='None' when User logs in.
Then when User selects any particular Company to work with that Selected_Company variable gets that company's name as a value.
All other views which show company related data have a django filter showing model objects matching with the value of Selected_Company.
If you know any other proper way to do it, I'd appreciate your advice.
Thx
Option 1:
One way is to have the URLs structure in a way that the company PK is present in the URL. For example:
/accounts/login/ # login; no company selected
/company/ # list of companies; no company selected
/company/14/ # company 14 was selected
/company/14/invoices/ # list of invoices of company 14
/company/14/invoices/create/ # add an invoice for company 14
/company/14/purchases/
/company/14/sales/
The urlconf would look like this:
...
url(
r'^company/',
include([
url(r'^$', views.CompanyListView.as_view()),
url(
r'^(?P<company_id>[0-9]+)/',
include([
url(r'^$', views.CompanyReadView.as_view()),
url(r'^invoices/$', views.InvoiceListView.as_view()),
url(r'^invoices/create/$', views.InvoiceCreateView.as_view()),
...
I hope that explains the point I am trying to make. The filtering in the views would be done using the company PK that is obtained from the URL. In the view code, that could look like this:
class InvoiceListView(ListView):
model = Invoice
def get_queryset(self):
return super().get_queryset().filter(
company_id=self.request.kwargs['company_id'])
Option 2:
Another way I could think of is setting the selected company in the session and using the session info to filter in the views. Maybe like this:
class InvoiceListView(ListView):
model = Invoice
def get_queryset(self):
return super().get_queryset().filter(
company_id=self.request.session['company_id'])

Need Guidance for django wagtail

currently i'm trying to Integrating Wagtail with existing django project.
I'm new in wagtail, and still learning about wagtail
class BlogPage(Page):
body = RichTextField(blank=True)
categories = ParentalManyToManyField('blog.BlogCategory', blank=True)
location = models.ForeignKey('blog.Location', on_delete=models.PROTECT)
and then i register the category and the location model as snippets.
how's the best practice for build page contains of BlogPage with
certain category / location ?
and how to call that page from django's menu
or maybe where can i find documentation for integrating wagtail to existing django project
Thank you
I think you're looking for a Blog Listing Page, where you can list all your blog posts and then have blog posts based on a certain category.
You'll probably want to use a RoutablePageMixin (if you're not creating an SPA with Vue or React). A RoutablePageMixin lets you automatically create additional child pages, without having to create Wagtail child pages.
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
class BlogListingPage(RoutablePageMixin, Page):
"""BlogListingPage class."""
template = 'cms/blog/blog_listing_page.html'
subpage_types = ['pages.BlogPage']
# ... other fields here
#route(r'^category/(?P<cat_slug>[-\w]*)/$', name='category_list')
def category_list(self, request, cat_slug):
"""Return posts in a certain category."""
context = self.get_context(request)
posts = BlogPage.objects.live().filter(categories__slug=cat_slug).order_by('-pub_date')
context['posts'] = posts
return render(request, 'cms/blog/blog_category_page.html', context)
Note I did not test this code, you may need to fix any errors and adjust this for your needs.
The above code will take your blog listing page (say its localhost:8000/blog/) and create a category listing page (ie. localhost:8000/blog/category/topic-slug/)
That topic-slug will be passed into the category_list() method, where you can then filter your BlogPage's based on the category(ies) it's in. It will add posts to your page, and render a different listing page, where you can customize your template.
It's been a while since I checked, but the Wagtail Bakery Demo probably has examples of this in there (and a lot of really sweet goodies).
You can read more about Wagtail Routable Pages at https://docs.wagtail.io/en/latest/reference/contrib/routablepage.html as well.

Django - multiple users with unique data

I'm building a SaaS that is something like a property maintenance application. A property maintenance company would use my application to manage multiple buildings and a whole bunch of tenants. So we have buildings and tenants all managed by one management company. However, a management company might have multiple staff (users) who are all managing these buildings/tenants.
Since this is a SaaS, I want to have a whole bunch of management companies all use my application to manage their own set of buildings/tenants. Thus, each management company needs to only be able to see their data and have no way to access another management company's data.
I have no idea how I might accomplish this in Django, though I have an idea:
Perhaps have a model called something like ManagementCompany and all buildings and tenants have a ForeignKey field which points to that ManagementCompany model. The staff of that management company would be connected to the ManagementCompany record via ForeignKey. In this way, the users can only see buildings/tenants whose ManagementCompany matches the users's.
Would that be a reasonable approach, or is there a better way?
Have you thought of subdomains?
Custom subdomains are a great way to provide customization for customers of SaaS products and to differentiate content without resorting to long URL paths.
So when a company registers, you either take the company name call to_lower() method, or prompt them to choose the one they like, just like Slack does, or Jira. I suggest you read this article Using Subdomains in Django Applications
It depends on how you want to store data in you SaaS application - single db for all instances or multiple db (each instance have separate database). Multiple db approach is pain when you want add new features, migrations etc. Single db is easier to manage but you need to add bunch of ForeignKey for each model.
For single db you will need:
Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
Database router that will return database name for read/write depending on SaaS instance.
That's all. Django will read/write to separate databases.
For multiple db you will need:
Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
Because you probably don't want to add ForeignKey to each model manually and filter it manually:
Abstract model with ForeignKey and custom save method to auto set that ForeignKey.
Custom model manager with custom get_queryset method that will filter all ORM queries with current SaaS instance. This manager should overide create method to auto set ForeignKey for queries like this: Foo.objects.create(**data)
Each model that will be fitlered for SaaS instance should inherit from that abstract model and you will need to set this model manager to that custom model manager.
That's all. Django will filter you ORM queries for current SaaS instance.
Example middleware (uses Domain model to check if domain exists, if not you will get HTTP404):
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def get_current_saas_instance():
return getattr(_thread_locals, 'current_instance', None)
class SaaSSubdomainMiddleware(object):
def process_request(self, request):
_thread_locals.current_instance = None
host = request.get_host()
try:
domain = Domain.objects.get(name=host)
_thread_locals.current_instance = domain.company
except:
logger.error('Error when checking SaaS domain', exc_info=True)
raise Http404
Example abstract model:
class SaaSModelAbstract(Model):
SAAS_FIELD_NAME = 'company'
company = ForeignKey(Company, null=True, blank=True)
class Meta:
abstract = True
def save(self, *args, **kwargs):
from .middleware import get_current_saas_instance
self.company = get_current_saas_instance()
super(SaaSModelAbstract, self).save(*args, **kwargs)
Example model manager:
class CurrentSaaSInstanceManager(models.Manager):
def get_current_saas_instance(self):
from .middleware import get_current_saas_instance
return get_current_saas_instance()
def get_queryset(self):
current_instance = self.get_current_saas_instance()
if current_instance is not None:
return super(CurrentSaaSInstanceManager, self).get_queryset().filter(
**{self.model.SAAS_FIELD_NAME: current_instance})
return super(CurrentSaaSInstanceManager, self).get_queryset()
def create(self, **kwargs):
current_instance = self.get_current_saas_instance()
if current_instance is not None:
kwargs[self.model.SAAS_FIELD_NAME] = current_instance
instance = self.model(**kwargs)
self._for_write = True
instance.save(force_insert=True, using=self.db)
return instance
Example models:
class FooModel(SaaSModelAbstract):
# model fields, methods
objects = CurrentSaaSInstanceManager()
class BarModel(models.Model):
# model fields, methods
pass
Example queries:
FooModel.objects.all() # will return query with all objects for current SaaS instance
BarModel.objects.all() # will return all objects withoout SaaS filtering
# Create objects for SaaS instance:
FooModel.objects.create(**data)
# or:
foo = FooModel()
foo.save()
In both cases (single/multiple db) django admin will be working properly.
I'm not posted db router because implementation is trivial and all you need can be found in django docs.
You can surely use django-tenant-schemas
Edit:
Given that you mentioned in the comment to my original answer that the database being used is MySQL, django-tenant-schemas is useless in your case. How about using multiple databases with database routers, that ways there can be separate databases for every company and using database routers you can route request for your databases through it.
It might be overwork but you probably can figure out a slick way to do it.

How Do I Capture the Subdomain in a Django URL Pattern?

I have a Django app that currently supports multiple languages. I'd like to add subdomain support so that going to 'de.mysite.com' queries articles in German, while 'mysite.com' queries things in English (the default language). There will be about 20 subdomains all pointing to the same Django app.
I have an abstract model with all of the fields for my data and a derived model for each language. Each language has its own database table, like so:
class ArticleBase(models.Model):
title = models.CharField(max_length=240, unique=True)
date_added = models.DateField(auto_now_add=True)
class Meta:
abstract = True
# This is English, the default.
class Article(ArticleBase):
pass
class Article_de(ArticleBase):
pass
I can get articles like this (I have this working today):
def article(request, title, language=None):
if language:
mod = get_model('app', 'Article_' + language)
items = mod.filter(title=title)
else:
items = Article.objects.filter(title=title)
This is my current URL pattern:
url(r'^article/(?P<title>[a-zA-Z_-]+)/$", 'app.views.article', name='article'),
How can I parse the subdomain prefix in a URL pattern so it can be passed into the article view? Or should I be getting this info from the request when I'm processing the view?
The URLS in django don't have access to the domain section of the URL, so that is not an option.
Doing it manually in each view is doable but not so easy to maintain.
I think that a custom middleware solution is probably better where you check the request headers in the middleware and then load the appropriate language (or you replace the accept-language headers and let django do it's magic)
The localemiddleware can make the language available which you can then easily use to pass through to the models/queries as needed.
Also if I may ask: why store every entry in a separate table/model? Can't you just add a language field to the model and store everything in one table? Because going for 20 or so models for articles can become harder to maintain than just 1 model.
UPDATE:
I've played around a little bit and the middleware required is quite simple (although my testing was quite limited):
class SubDomainLanguage(object):
def process_request(self, request):
try:
request.session['django_language'] = request.META['HTTP_HOST'].split('.')[0]
except KeyError:
pass
is the middleware and then change the settings to include the following:
MIDDLEWARE_CLASSES = (
...
'django.contrib.sessions.middleware.SessionMiddleware',
'subdomainmiddleware.SubDomainLanguage',
'django.middleware.locale.LocaleMiddleware',
...
)
This way you can then leverage the normal Django builtin i18n support.
In the views the language is then available in request.LANGUAGE_CODE
I have a case that I have subdomains with periods such as 1.2.3.mydomain.net. I know my domain and set it in my local_settings.py as:
DOMAIN = 'EXAMPLE.net'
TEST_SUBDOMAINS = ["127.0.0.1", "localhost", "testserver"]
I then check that I don't do anything if its for test purposes.
After that I get my sub domain as follows:
if (DOMAIN in TEST_SUBDOMAINS):
return HttpResponse("test only")
subdomain = request.META['HTTP_HOST'].replace((DOMAIN), "")[:-1]
# -1 is to remove the trailing "." from the subdomain
if subdomain and subdomain != "www" :
# whatever you need...
You can just use this easy to use django-subdomains library. It also support subdomain-based URL routing and reversing.

MongoEngine query to get Post's author and Comments authors for Posts (at the same time)

I'm working on a web project with Django and MongoDB as my database (using MongoEngine to connect them).
I have to create Celery task to cleanup old user accounts. I need to only clean lazy users accounts without any content after one month (lazy users are automatically created users when they first connect to the website). What count as content? Any posts from the user or comments on any of the posts.
I did it like this, but I want to transform this into query if possible:
def clean_inactive_lazy_users():
users_with_content = []
for post in api_models.Post.objects:
users_with_content.append(post.author)
for comment in post.comments:
users_with_content.append(comment.author)
users_with_content = list(set(users_with_content))
for user in account_models.User.objects:
if not user.is_authenticated() and (timezone.now() - user.connection_last_unsubscribe).days >= settings.DELETE_LAZY_USER_AFTER_DAYS and user not in users_with_content:
user.delete()
The models look like this:
base.py
class AuthoredEmbeddedDocument(mongoengine.EmbeddedDocument):
author = mongoengine.ReferenceField(models.User, required=True)
class AuthoredDocument(mongoengine.Document):
author = mongoengine.ReferenceField(models.User, required=True)
api_models:
from . import base
class Comment(base.AuthoredEmbeddedDocument):
"""
This class defines document type for comments on posts.
"""
class Post(base.AuthoredDocument):
"""
This class defines document type for posts.
"""
account_models:
class User(auth.User):
def is_authenticated(self):
return self.has_usable_password()
Hopefully I provided enough information so you can help me with the problem. Thanks!
I think there are a couple ways for you to clean this up.
You could get all author unique ids of posts with something like:
user_ids_with_posts_list = Posts.objects.scalar('author.id', flat=True).distinct('author.id')
scalar should give you a list of author ids instead of document objects and distinct
should make sure they are unique. This pushes what you are doing in python to mongodb
You could then construct a query for Users. You would hvae to change your days ago into a date. What condition is has_usable_password checking for?
start_time = timezone.now() - timedelta(days=DAYS_AGO_CONSTANT)
invalid_users = User.objects.filter(connection_last_unsubscribe__lte=start_time,
password__isnull=True).exclude(pk__in=user_ids_with_posts_list)
invalid_users.delete()