From 5f1a856563acc1ae1dd13cf228ef140924fd6896 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sat, 29 Jun 2024 21:37:44 -0400 Subject: [PATCH] * askbot/conf/group_settings.py: - adds setting PER_EMAIL_DOMAIN_GROUPS_ENABLED - adds setting PER_EMAIL_DOMAIN_GROUP_DEFAULT_VISIBILITY - visibility of groups created for email domains options: 0 - admins, 1 - mods, 2 - members + admins and mods, 3 - public (default) * askbot/const/__init__.py: - adds constants for group visibility, PEP8 * askbot/models/analytics.py: - adds models for daily summaries for users and groups - models BaseSummary (abstract), DailySummary (abstract), UserDailySummary, GroupDailySummary * askbot/models/user.py: - adds function get_organization_name_from_domain - adds visibility field to Askbot Group * adds management command askbot_create_per_email_domain_groups * migration 0028: - adds UserDailySummary and GroupDailySummary models * migration 0029: - adds visibility field to the Askbot Group model todo: add tests for the askbot_create_per_email_domain_groups, Group.objects.get_or_create --- askbot/conf/group_settings.py | 24 ++++++++- askbot/const/__init__.py | 18 +++++-- .../askbot_create_per_email_domain_groups.py | 27 ++++++++++ ...0028_userdailysummary_groupdailysummary.py | 51 +++++++++++++++++++ askbot/migrations/0029_group_visibility.py | 18 +++++++ askbot/models/analytics.py | 39 +++++++++++++- askbot/models/user.py | 31 +++++++---- askbot/tests/test_markup.py | 5 +- 8 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 askbot/management/commands/askbot_create_per_email_domain_groups.py create mode 100644 askbot/migrations/0028_userdailysummary_groupdailysummary.py create mode 100644 askbot/migrations/0029_group_visibility.py diff --git a/askbot/conf/group_settings.py b/askbot/conf/group_settings.py index ff0d71ba36..f6a01a0bdb 100644 --- a/askbot/conf/group_settings.py +++ b/askbot/conf/group_settings.py @@ -1,8 +1,9 @@ """Group settings""" from django.utils.translation import gettext_lazy as _ +from livesettings import values as livesettings from askbot.conf.settings_wrapper import settings from askbot.conf.super_groups import LOGIN_USERS_COMMUNICATION -from livesettings import values as livesettings +from askbot import const GROUP_SETTINGS = livesettings.ConfigurationGroup( 'GROUP_SETTINGS', @@ -64,3 +65,24 @@ def group_name_update_callback(old_name, new_name): '"group-name@domain.com"') ) ) + +settings.register( + livesettings.BooleanValue( + GROUP_SETTINGS, + 'PER_EMAIL_DOMAIN_GROUPS_ENABLED', + default=False, + description=_('Enable per email domain user groups'), + help_text=_('If enabled, groups will be created for each email domain name') + ) +) + +settings.register( + livesettings.StringValue( + GROUP_SETTINGS, + 'PER_EMAIL_DOMAIN_GROUP_DEFAULT_VISIBILITY', + choices=const.GROUP_VISIBILITY_CHOICES, + default=const.GROUP_VISIBILITY_PUBLIC, + description=_('Default visibility of groups created for the email domains'), + help_text=_('Administrators can change the visibility of these groups individually later') + ) +) diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 9e02a2ae59..980e6324bd 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -179,11 +179,11 @@ TAG_CHARS = r'\wp{M}+.#-' TAG_FIRST_CHARS = r'[\wp{M}]' TAG_FORBIDDEN_FIRST_CHARS = r'#' -TAG_REGEX_BARE = r'%s[%s]+' % (TAG_FIRST_CHARS, TAG_CHARS) -TAG_REGEX = r'^%s$' % TAG_REGEX_BARE +TAG_REGEX_BARE = rf'{TAG_FIRST_CHARS}[{TAG_CHARS}]+' +TAG_REGEX = rf'^{TAG_REGEX_BARE}$' TAG_STRIP_CHARS = ', ' -TAG_SPLIT_REGEX = r'[%s]+' % TAG_STRIP_CHARS +TAG_SPLIT_REGEX = rf'[{TAG_STRIP_CHARS}]+' TAG_SEP = ',' # has to be valid TAG_SPLIT_REGEX char and MUST NOT be in const.TAG_CHARS #!!! see const.message_keys.TAG_WRONG_CHARS_MESSAGE @@ -649,3 +649,15 @@ """ PROFILE_WEBSITE_URL_MAX_LENGTH = 200 + +GROUP_VISIBILITY_ADMINS = 0 +GROUP_VISIBILITY_MODS = 1 +GROUP_VISIBILITY_MEMBERS = 2 +GROUP_VISIBILITY_PUBLIC = 3 + +GROUP_VISIBILITY_CHOICES = ( + (GROUP_VISIBILITY_ADMINS, _('Visible to administrators')), + (GROUP_VISIBILITY_MODS, _('Visible to moderators and administrators')), + (GROUP_VISIBILITY_MEMBERS, _('Visible to own members, moderators and administrators')), + (GROUP_VISIBILITY_PUBLIC, _('Visible to everyone')), +) diff --git a/askbot/management/commands/askbot_create_per_email_domain_groups.py b/askbot/management/commands/askbot_create_per_email_domain_groups.py new file mode 100644 index 0000000000..635029b98e --- /dev/null +++ b/askbot/management/commands/askbot_create_per_email_domain_groups.py @@ -0,0 +1,27 @@ +"""A management command that creates groups for each email domain in the database.""" +from django.core.management.base import BaseCommand +from askbot.conf import settings +from askbot.models import User +from askbot.models.analytics import get_organization_domains +from askbot.models.user import get_organization_name_from_domain +from askbot.utils.console import ProgressBar + +class Command(BaseCommand): # pylint: disable=missing-docstring + help = 'Create groups for each email domain in the database.' + + def handle(self, *args, **options): # pylint: disable=missing-docstring, unused-argument + """Obtains a list of unique email domains names. + Creates a group for each domain name, if such group does not exist. + Group visibility is set to the value of settings.PER_EMAIL_DOMAIN_GROUP_DEFAULT_VISIBILITY. + """ + domains = get_organization_domains() + count = len(domains) + message = 'Initializing groups by the email address domain names' + for domain in ProgressBar(domains, count, message): + organization_name = get_organization_name_from_domain(domain) + group = User.objects.get_or_create_group( + organization_name, + visibility=settings.PER_EMAIL_DOMAIN_GROUP_DEFAULT_VISIBILITY + ) + print('Group {0} created.'.format(group.name)) + diff --git a/askbot/migrations/0028_userdailysummary_groupdailysummary.py b/askbot/migrations/0028_userdailysummary_groupdailysummary.py new file mode 100644 index 0000000000..88abbff2fa --- /dev/null +++ b/askbot/migrations/0028_userdailysummary_groupdailysummary.py @@ -0,0 +1,51 @@ +# Generated by Django 4.2.4 on 2024-06-24 21:15 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('askbot', '0027_populate_analytics_events'), + ] + + operations = [ + migrations.CreateModel( + name='UserDailySummary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num_questions', models.PositiveIntegerField()), + ('num_answers', models.PositiveIntegerField()), + ('num_upvotes', models.PositiveIntegerField()), + ('num_downvotes', models.PositiveIntegerField()), + ('question_views', models.PositiveIntegerField()), + ('time_on_site', models.DurationField()), + ('date', models.DateField(db_index=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='GroupDailySummary', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('num_questions', models.PositiveIntegerField()), + ('num_answers', models.PositiveIntegerField()), + ('num_upvotes', models.PositiveIntegerField()), + ('num_downvotes', models.PositiveIntegerField()), + ('question_views', models.PositiveIntegerField()), + ('time_on_site', models.DurationField()), + ('date', models.DateField(db_index=True)), + ('num_users', models.PositiveIntegerField()), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='askbot.group')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/askbot/migrations/0029_group_visibility.py b/askbot/migrations/0029_group_visibility.py new file mode 100644 index 0000000000..7252db4eeb --- /dev/null +++ b/askbot/migrations/0029_group_visibility.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.4 on 2024-06-24 21:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('askbot', '0028_userdailysummary_groupdailysummary'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='visibility', + field=models.SmallIntegerField(choices=[(0, 'Visible to administrators'), (1, 'Visible to moderators and administrators'), (2, 'Visible to own members, moderators and administrators'), (3, 'Visible to everyone')], default=3), + ), + ] diff --git a/askbot/models/analytics.py b/askbot/models/analytics.py index ff9ca81174..e1ff49d7f2 100644 --- a/askbot/models/analytics.py +++ b/askbot/models/analytics.py @@ -8,6 +8,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.conf import settings as django_settings from django.utils.translation import gettext_lazy as _ +from askbot.models.user import Group as AskbotGroup #for convenience, here are the activity types from used in the Activity object #TYPE_ACTIVITY_ASK_QUESTION = 1 @@ -118,6 +119,7 @@ def __str__(self): email = self.user.email # pylint: disable=no-member return f"Session: {email} {created_at} - {updated_at}" + class Event(models.Model): """Analytics event""" session = models.ForeignKey(Session, on_delete=models.CASCADE) @@ -129,4 +131,39 @@ class Event(models.Model): def __str__(self): timestamp = self.timestamp.isoformat() # pylint: disable=no-member - return f"Event: {self.event_type_display} {timestamp}" + return f"Event: {self.event_type_display} {timestamp}" # pylint: disable=no-member + + +class BaseSummary(models.Model): + """ + An abstract model for per-interval summaries. + An interval name is defined in the subclass. + """ + num_questions = models.PositiveIntegerField() + num_answers = models.PositiveIntegerField() + num_upvotes = models.PositiveIntegerField() + num_downvotes = models.PositiveIntegerField() + question_views = models.PositiveIntegerField() + time_on_site = models.DurationField() + + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + abstract = True + + +class DailySummary(BaseSummary): + """An abstract class for daily summaries.""" + date = models.DateField(db_index=True) + + class Meta: # pylint: disable=too-few-public-methods, missing-class-docstring + abstract = True + + +class UserDailySummary(DailySummary): + """User summary for each day with activity.""" + user = models.ForeignKey(User, on_delete=models.CASCADE) + + +class GroupDailySummary(DailySummary): + """Group summary for each day with activity.""" + group = models.ForeignKey(AskbotGroup, on_delete=models.CASCADE) + num_users = models.PositiveIntegerField() diff --git a/askbot/models/user.py b/askbot/models/user.py index 31809ae244..6a87cb2dce 100644 --- a/askbot/models/user.py +++ b/askbot/models/user.py @@ -1,28 +1,33 @@ import datetime import logging -import re +from collections import defaultdict from django.db import models -from django.db.models import Q from django.db.utils import IntegrityError from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import fields from django.contrib.auth.models import User from django.contrib.auth.models import Group as AuthGroup from django.core import exceptions -from django.forms import EmailField, URLField +from django.forms import EmailField from django.utils import translation, timezone from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy -from django.utils.html import strip_tags from askbot import const from askbot.conf import settings as askbot_settings from askbot.utils import functions from askbot.models.base import BaseQuerySetManager -from askbot.models.analytics import Session -from collections import defaultdict +from askbot.models.tag import get_tags_by_names, Tag PERSONAL_GROUP_NAME_PREFIX = '_personal_' +def get_organization_name_from_domain(domain): + """Returns organization name from domain. + The organization name is the second level domain name, + sentence-cased. + """ + base_domain = domain.split('.')[-2] + return base_domain.capitalize() + class InvitedModerator(object): """Mock user class to represent invited moderators""" def __init__(self, username, email): @@ -606,6 +611,9 @@ class Group(AuthGroup): can_upload_images = models.BooleanField(default=False) openness = models.SmallIntegerField(default=CLOSED, choices=OPENNESS_CHOICES) + visibility = models.SmallIntegerField(default=const.GROUP_VISIBILITY_PUBLIC, + choices=const.GROUP_VISIBILITY_CHOICES) + # preapproved email addresses and domain names to auto-join groups # trick - the field is padded with space and all tokens are space separated preapproved_emails = models.TextField( @@ -710,7 +718,8 @@ def save(self, *args, **kwargs): super(Group, self).save(*args, **kwargs) -class BulkTagSubscriptionManager(BaseQuerySetManager): +class BulkTagSubscriptionManager(BaseQuerySetManager): # pylint: disable=too-few-public-methods + """Manager class for the BulkTagSubscription model""" def create( self, @@ -730,7 +739,6 @@ def create( tag_name_list = [] if tag_names: - from askbot.models.tag import get_tags_by_names tags, new_tag_names = get_tags_by_names(tag_names, language_code) if new_tag_names: assert(tag_author) @@ -738,7 +746,6 @@ def create( tags_id_list= [tag.id for tag in tags] tag_name_list = [tag.name for tag in tags] - from askbot.models.tag import Tag new_tags = Tag.objects.create_in_bulk( tag_names=new_tag_names, user=tag_author, @@ -771,6 +778,7 @@ def create( class BulkTagSubscription(models.Model): + """Subscribes users in bulk to a list of tags""" date_added = models.DateField(auto_now_add=True) tags = models.ManyToManyField('Tag') users = models.ManyToManyField(User) @@ -779,9 +787,10 @@ class BulkTagSubscription(models.Model): objects = BulkTagSubscriptionManager() def tag_list(self): - return [tag.name for tag in self.tags.all()] + """Returns list of tag names""" + return [tag.name for tag in self.tags.all()] # pylint: disable=no-member - class Meta: + class Meta: # pylint: disable=too-few-public-methods, missing-docstring app_label = 'askbot' ordering = ['-date_added'] diff --git a/askbot/tests/test_markup.py b/askbot/tests/test_markup.py index 6d38549727..55d09a0546 100644 --- a/askbot/tests/test_markup.py +++ b/askbot/tests/test_markup.py @@ -130,4 +130,7 @@ def test_convert_mixed_text(self): """ """
http://example.com
""" - self.assertHTMLEqual(self.conv(text), expected) + import pdb + pdb.set_trace() + converted = self.conv(text) + self.assertHTMLEqual(converted, expected)