import random from datetime import date, timedelta from datetime import timezone as tz from typing import Iterator from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib.auth.hashers import make_password from django.core.management.base import BaseCommand from django.db.models import Count, Exists, Min, OuterRef, Subquery from django.utils.timezone import localdate, make_aware, now from faker import Faker from club.models import Club, Membership from core.models import Group, User from counter.models import ( Counter, Customer, Permanency, Product, ProductType, Refilling, Selling, ) from forum.models import Forum, ForumMessage, ForumTopic from pedagogy.models import UV from subscription.models import Subscription class Command(BaseCommand): help = "Add more fixtures for a more complete development environment" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.faker = Faker("fr_FR") def handle(self, *args, **options): if not settings.DEBUG: raise Exception("Never call this command in prod. Never.") self.stdout.write("Creating users...") users = self.create_users() subscribers = random.sample(users, k=int(0.8 * len(users))) self.stdout.write("Creating subscriptions...") self.create_subscriptions(subscribers) self.stdout.write("Creating club memberships...") users_qs = User.objects.filter(id__in=[s.id for s in subscribers]) subscribers_now = list( users_qs.annotate( filter=Exists( Subscription.objects.filter( member_id=OuterRef("pk"), subscription_end__gte=now() ) ) ) ) old_subscribers = list( users_qs.annotate( filter=Exists( Subscription.objects.filter( member_id=OuterRef("pk"), subscription_end__lt=now() ) ) ) ) self.make_club( Club.objects.get(unix_name="ae"), random.sample(subscribers_now, k=min(30, len(subscribers_now))), random.sample(old_subscribers, k=min(60, len(old_subscribers))), ) self.make_club( Club.objects.get(unix_name="troll"), random.sample(subscribers_now, k=min(20, len(subscribers_now))), random.sample(old_subscribers, k=min(80, len(old_subscribers))), ) self.stdout.write("Creating uvs...") self.create_uvs() self.stdout.write("Creating products...") self.create_products() self.stdout.write("Creating sales and refills...") sellers = random.sample(list(User.objects.all()), 100) self.create_sales(sellers) self.stdout.write("Creating permanences...") self.create_permanences(sellers) self.stdout.write("Filling the forum...") self.create_forums() self.stdout.write("Done") def create_users(self) -> list[User]: password = make_password("plop") users = [ User( username=self.faker.user_name(), first_name=self.faker.first_name(), last_name=self.faker.last_name(), date_of_birth=self.faker.date_of_birth(minimum_age=15, maximum_age=25), email=self.faker.email(), phone=self.faker.phone_number(), address=self.faker.address(), password=password, ) for _ in range(600) ] # there may a duplicate or two # Not a problem, we will just have 599 users instead of 600 users = User.objects.bulk_create(users, ignore_conflicts=True) users = list(User.objects.order_by("-id")[: len(users)]) public_group = Group.objects.get(pk=settings.SITH_GROUP_PUBLIC_ID) public_group.users.add(*users) return users def create_subscriptions(self, users: list[User]): def prepare_subscription(_user: User, start_date: date) -> Subscription: payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0] duration = random.randint(1, 4) sub = Subscription(member=_user, payment_method=payment_method) sub.subscription_start = sub.compute_start(d=start_date, duration=duration) sub.subscription_end = sub.compute_end(duration) return sub subscriptions = [] customers = [] # first set of subscriptions for i, user in enumerate(users): sub = prepare_subscription(user, self.faker.past_date("-10y")) subscriptions.append(sub) customers.append( Customer( user=user, account_id=f"{9900 + i}{self.faker.random_lowercase_letter()}", ) ) while sub.subscription_end < localdate() and random.random() > 0.7: # 70% chances to subscribe again # (expect if it would make the subscription start after tomorrow) sub = prepare_subscription( user, self.faker.past_date(sub.subscription_end) ) subscriptions.append(sub) old_subscriber_group = Group.objects.get( pk=settings.SITH_GROUP_OLD_SUBSCRIBERS_ID ) old_subscriber_group.users.add(*users) Subscription.objects.bulk_create(subscriptions) Customer.objects.bulk_create(customers, ignore_conflicts=True) def make_club(self, club: Club, members: list[User], old_members: list[User]): def zip_roles(users: list[User]) -> Iterator[tuple[User, int]]: roles = iter(sorted(settings.SITH_CLUB_ROLES.keys(), reverse=True)) user_idx = 0 while (role := next(roles)) > 2: # one member for each major role yield users[user_idx], role user_idx += 1 for _ in range(int(0.3 * (len(users) - user_idx))): # 30% of the remaining in the board yield users[user_idx], 2 user_idx += 1 for remaining in users[user_idx + 1 :]: # everything else is a simple member yield remaining, 1 memberships = [] old_members = old_members.copy() random.shuffle(old_members) for old in old_members: start = self.faker.date_between("-3y", "-1y") memberships.append( Membership( start_date=start, end_date=self.faker.past_date(start), user=old, role=random.choice(list(settings.SITH_CLUB_ROLES.keys())), club=club, ) ) for member, role in zip_roles(members): start = self.faker.past_date("-1y") memberships.append( Membership( start_date=start, user=member, role=role, club=club, ) ) memberships = Membership.objects.bulk_create(memberships) Membership._add_club_groups(memberships) def create_uvs(self): root = User.objects.get(username="root") categories = ["CS", "TM", "OM", "QC", "EC"] branches = ["TC", "GMC", "GI", "EDIM", "E", "IMSI", "HUMA"] languages = ["FR", "FR", "EN"] semesters = ["AUTUMN", "SPRING", "AUTUMN_AND_SPRING"] teachers = [self.faker.name() for _ in range(50)] uvs = [] for _ in range(1000): code = ( self.faker.random_uppercase_letter() + self.faker.random_uppercase_letter() + str(random.randint(10, 90)) ) uvs.append( UV( code=code, author=root, manager=random.choice(teachers), title=self.faker.text(max_nb_chars=50), department=random.choice(branches), credit_type=random.choice(categories), credits=6, semester=random.choice(semesters), language=random.choice(languages), program=self.faker.paragraph(random.randint(3, 10)), skills="\n* ".join(self.faker.sentences(random.randint(3, 10))), key_concepts="\n* ".join( self.faker.sentences(random.randint(3, 10)) ), hours_CM=random.randint(15, 40), hours_TD=random.randint(15, 40), hours_TP=random.randint(15, 40), hours_THE=random.randint(15, 40), hours_TE=random.randint(15, 40), ) ) UV.objects.bulk_create(uvs, ignore_conflicts=True) def create_products(self): categories = [ ProductType(name=self.faker.text(max_nb_chars=30)) for _ in range(10) ] ProductType.objects.bulk_create(categories) categories = list( ProductType.objects.filter(name__in=[c.name for c in categories]) ) ae = Club.objects.get(unix_name="ae") other_clubs = random.sample(list(Club.objects.all()), k=3) groups = list( Group.objects.filter(name__in=["Subscribers", "Old subscribers", "Public"]) ) counters = list( Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette", "Eboutic"]) ) # 2/3 of the products are owned by AE clubs = [ae, ae, ae, ae, ae, ae, *other_clubs] products = [] buying_groups = [] selling_places = [] for _ in range(200): price = random.randint(0, 10) + random.choice([0, 0.25, 0.5, 0.75]) product = Product( name=self.faker.text(max_nb_chars=30), description=self.faker.text(max_nb_chars=120), product_type=random.choice(categories), code="".join(self.faker.random_letters(length=random.randint(4, 8))), purchase_price=price, selling_price=price, special_selling_price=price - min(0.5, price), club=random.choice(clubs), limit_age=0 if random.random() > 0.2 else 18, archived=bool(random.random() > 0.7), ) products.append(product) # there will be products without buying groups # but there are also such products in the real database buying_groups.extend( Product.buying_groups.through(product=product, group=group) for group in random.sample(groups, k=random.randint(0, 3)) ) selling_places.extend( Counter.products.through(counter=counter, product=product) for counter in random.sample(counters, random.randint(0, 4)) ) Product.objects.bulk_create(products) Product.buying_groups.through.objects.bulk_create(buying_groups) Counter.products.through.objects.bulk_create(selling_places) def create_sales(self, sellers: list[User]): customers = list( Customer.objects.annotate( since=Subquery( Subscription.objects.filter(member__customer=OuterRef("pk")) .annotate(res=Min("subscription_start")) .values("res") .order_by("res")[:1] ) ) ) products = list(Product.objects.all()) counters = list( Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette"]) ) sales = [] reloads = [] for customer in customers: # the longer the customer has existed, the higher the mean of nb_products mu = 5 + (now().year - customer.since.year) * 2 nb_sales = max(0, int(random.normalvariate(mu=mu, sigma=mu * 5))) favoured_products = random.sample(products, k=(random.randint(1, 5))) favoured_counter = random.choice(counters) this_customer_sales = [] for _ in range(nb_sales): product = ( random.choice(favoured_products) if random.random() > 0.7 else random.choice(products) ) counter = ( favoured_counter if random.random() > 0.7 else random.choice(counters) ) this_customer_sales.append( Selling( product=product, counter=counter, club_id=product.club_id, quantity=random.randint(1, 5), unit_price=product.selling_price, seller=random.choice(sellers), customer=customer, date=make_aware( self.faker.date_time_between(customer.since, localdate()) ), ) ) total_expanse = sum(s.unit_price * s.quantity for s in this_customer_sales) total_reloaded = 0 while total_reloaded < total_expanse: amount = random.choice(list(range(5, 51, 5))) total_reloaded += amount reloads.append( Refilling( counter=random.choice(counters), amount=amount, operator=random.choice(sellers), customer=customer, date=make_aware( self.faker.date_time_between(customer.since, localdate()) ), is_validated=True, ) ) sales.extend(this_customer_sales) Refilling.objects.bulk_create(reloads) Selling.objects.bulk_create(sales) Customer.objects.update_amount() def create_permanences(self, sellers: list[User]): counters = list( Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette"]) ) perms = [] for seller in sellers: favoured_counter = random.choice(counters) nb_perms = abs(int(random.normalvariate(mu=275, sigma=100))) active_period_start = self.faker.past_date("-10y") active_period_end = self.faker.date_between( active_period_start, min(localdate(), active_period_start + relativedelta(years=5)), ) for _ in range(nb_perms): counter = ( favoured_counter if random.random() > 0.8 else random.choice(counters) ) duration = self.faker.time_delta(timedelta(hours=1)) start = make_aware( self.faker.date_time_between(active_period_start, active_period_end) ) perms.append( Permanency( counter=counter, user=seller, start=start, end=start + duration ) ) Permanency.objects.bulk_create(perms) def create_forums(self): forumers = random.sample(list(User.objects.all()), 100) most_actives = random.sample(forumers, 10) categories = list(Forum.objects.filter(is_category=True)) new_forums = [ Forum(name=self.faker.text(20), parent=random.choice(categories)) for _ in range(15) ] Forum.objects.bulk_create(new_forums) forums = list(Forum.objects.filter(is_category=False)) new_topics = [ ForumTopic( _title=self.faker.text(20), author=random.choice(most_actives), forum=random.choice(forums), ) for _ in range(100) ] ForumTopic.objects.bulk_create(new_topics) topics = list(ForumTopic.objects.all()) def get_author(): if random.random() > 0.5: return random.choice(most_actives) return random.choice(forumers) messages = [] for t in topics: nb_messages = max(1, int(random.normalvariate(mu=90, sigma=50))) dates = sorted( [ self.faker.date_time_between("-15y", "-1d", tzinfo=tz.utc) for _ in range(nb_messages) ], reverse=True, ) messages.extend( [ ForumMessage( topic=t, author=get_author(), date=d, message="\n\n".join( self.faker.paragraphs(random.randint(1, 4)) ), ) for d in dates ] ) ForumMessage.objects.bulk_create(messages) ForumTopic.objects.update( _message_number=Subquery( ForumMessage.objects.filter(topic_id=OuterRef("pk")) .values("topic_id") .annotate(res=Count("*")) .values("res") ), _last_message_id=Subquery( ForumMessage.objects.order_by("-date").values("id")[:1] ), ) for f in Forum.objects.filter(parent__isnull=False): # this is a N+1 queries, but it's ok, # since there are quite a few forums # and trying to do it with a single query # would result in a big whibbly-woobly hacky queryset f.set_last_message() f.set_topic_number()