class VerifiedEmail(models.Model): """Storage for verified e-mails from auth backends.""" social = models.ForeignKey(UserSocialAuth, on_delete=models.deletion.CASCADE) email = EmailField() class Meta: verbose_name = "Verified e-mail" verbose_name_plural = "Verified e-mails" def __str__(self): return f"{self.social.user.username} - {self.email}" @property def provider(self): return self.social.provider
class Profile(models.Model): """User profiles storage.""" user = models.OneToOneField(User, unique=True, editable=False, on_delete=models.deletion.CASCADE) language = models.CharField( verbose_name=_("Interface Language"), max_length=10, blank=True, choices=settings.LANGUAGES, ) languages = models.ManyToManyField( Language, verbose_name=_("Translated languages"), blank=True, help_text=_("Choose the languages you can translate to. " "These will be offered to you on the dashboard " "for easier access to your chosen translations."), ) secondary_languages = models.ManyToManyField( Language, verbose_name=_("Secondary languages"), help_text=_( "Choose languages you can understand, strings in those languages " "will be shown in addition to the source string."), related_name="secondary_profile_set", blank=True, ) suggested = models.IntegerField(default=0, db_index=True) translated = models.IntegerField(default=0, db_index=True) uploaded = models.IntegerField(default=0, db_index=True) commented = models.IntegerField(default=0, db_index=True) hide_completed = models.BooleanField( verbose_name=_("Hide completed translations on the dashboard"), default=False) secondary_in_zen = models.BooleanField( verbose_name=_("Show secondary translations in the Zen mode"), default=True) hide_source_secondary = models.BooleanField( verbose_name=_("Hide source if a secondary translation exists"), default=False) editor_link = models.CharField( default="", blank=True, max_length=200, verbose_name=_("Editor link"), help_text=_( "Enter a custom URL to be used as link to the source code. " "You can use {{branch}} for branch, " "{{filename}} and {{line}} as filename and line placeholders."), validators=[validate_editor], ) TRANSLATE_FULL = 0 TRANSLATE_ZEN = 1 translate_mode = models.IntegerField( verbose_name=_("Translation editor mode"), choices=((TRANSLATE_FULL, _("Full editor")), (TRANSLATE_ZEN, _("Zen mode"))), default=TRANSLATE_FULL, ) ZEN_VERTICAL = 0 ZEN_HORIZONTAL = 1 zen_mode = models.IntegerField( verbose_name=_("Zen editor mode"), choices=( (ZEN_VERTICAL, _("Top to bottom")), (ZEN_HORIZONTAL, _("Side by side")), ), default=ZEN_VERTICAL, ) special_chars = models.CharField( default="", blank=True, max_length=30, verbose_name=_("Special characters"), help_text=_( "You can specify additional special visual keyboard characters " "to be shown while translating. It can be useful for " "characters you use frequently, but are hard to type on your keyboard." ), ) nearby_strings = models.SmallIntegerField( verbose_name=_("Number of nearby strings"), default=settings.NEARBY_MESSAGES, validators=[MinValueValidator(1), MaxValueValidator(50)], help_text= _("Number of nearby strings to show in each direction in the full editor." ), ) DASHBOARD_WATCHED = 1 DASHBOARD_COMPONENT_LIST = 4 DASHBOARD_SUGGESTIONS = 5 DASHBOARD_COMPONENT_LISTS = 6 DASHBOARD_CHOICES = ( (DASHBOARD_WATCHED, _("Watched translations")), (DASHBOARD_COMPONENT_LISTS, _("Component lists")), (DASHBOARD_COMPONENT_LIST, _("Component list")), (DASHBOARD_SUGGESTIONS, _("Suggested translations")), ) DASHBOARD_SLUGS = { DASHBOARD_WATCHED: "your-subscriptions", DASHBOARD_COMPONENT_LIST: "list", DASHBOARD_SUGGESTIONS: "suggestions", DASHBOARD_COMPONENT_LISTS: "componentlists", } dashboard_view = models.IntegerField( choices=DASHBOARD_CHOICES, verbose_name=_("Default dashboard view"), default=DASHBOARD_WATCHED, ) dashboard_component_list = models.ForeignKey( "trans.ComponentList", verbose_name=_("Default component list"), on_delete=models.deletion.SET_NULL, blank=True, null=True, ) watched = models.ManyToManyField( "trans.Project", verbose_name=_("Watched projects"), help_text=_("You can receive notifications for watched projects and " "they are shown on the dashboard by default."), blank=True, ) # Public profile fields website = models.URLField( verbose_name=_("Website URL"), blank=True, ) liberapay = models.SlugField( verbose_name=_("Liberapay username"), blank=True, help_text=_("Liberapay is a platform to donate money to teams, " "organizations and individuals."), ) fediverse = models.URLField( verbose_name=_("Fediverse URL"), blank=True, help_text=_("Link to your Fediverse profile for federated services " "like Mastodon or diaspora*."), ) codesite = models.URLField( verbose_name=_("Code site URL"), blank=True, help_text=_( "Link to your code profile for services like Codeberg or GitLab."), ) github = models.SlugField( verbose_name=_("GitHub username"), blank=True, ) twitter = models.SlugField( verbose_name=_("Twitter username"), blank=True, ) linkedin = models.SlugField( verbose_name=_("LinkedIn profile name"), help_text=_( "Your LinkedIn profile name from linkedin.com/in/profilename"), blank=True, ) location = models.CharField( verbose_name=_("Location"), max_length=100, blank=True, ) company = models.CharField( verbose_name=_("Company"), max_length=100, blank=True, ) public_email = EmailField( verbose_name=_("Public e-mail"), blank=True, max_length=EMAIL_LENGTH, ) def __str__(self): return self.user.username def get_absolute_url(self): return self.user.get_absolute_url() def get_user_display(self): return get_user_display(self.user) def get_user_display_link(self): return get_user_display(self.user, True, True) def get_user_name(self): return get_user_display(self.user, False) def increase_count(self, item: str, increase: int = 1): """Updates user actions counter.""" # Update our copy setattr(self, item, getattr(self, item) + increase) # Update database update = {item: F(item) + increase} Profile.objects.filter(pk=self.pk).update(**update) @property def full_name(self): """Return user's full name.""" return self.user.full_name def clean(self): """Check if component list is chosen when required.""" # There is matching logic in ProfileBaseForm.add_error to ignore this # validation on partial forms if (self.dashboard_view == Profile.DASHBOARD_COMPONENT_LIST and self.dashboard_component_list is None): message = _( "Please choose which component list you want to display on " "the dashboard.") raise ValidationError({ "dashboard_component_list": message, "dashboard_view": message }) if (self.dashboard_view != Profile.DASHBOARD_COMPONENT_LIST and self.dashboard_component_list is not None): message = _( "Selecting component list has no effect when not shown on " "the dashboard.") raise ValidationError({ "dashboard_component_list": message, "dashboard_view": message }) def dump_data(self): def dump_object(obj, *attrs): return {attr: getattr(obj, attr) for attr in attrs} result = { "basic": dump_object(self.user, "username", "full_name", "email", "date_joined"), "profile": dump_object( self, "language", "suggested", "translated", "uploaded", "hide_completed", "secondary_in_zen", "hide_source_secondary", "editor_link", "translate_mode", "zen_mode", "special_chars", "dashboard_view", "dashboard_component_list", ), "auditlog": [ dump_object(log, "address", "user_agent", "timestamp", "activity") for log in self.user.auditlog_set.iterator() ], } result["profile"]["languages"] = [ lang.code for lang in self.languages.iterator() ] result["profile"]["secondary_languages"] = [ lang.code for lang in self.secondary_languages.iterator() ] result["profile"]["watched"] = [ project.slug for project in self.watched.iterator() ] return result @cached_property def primary_language_ids(self) -> Set[int]: return set(self.languages.values_list("pk", flat=True)) @cached_property def secondary_language_ids(self) -> Set[int]: return set(self.secondary_languages.values_list("pk", flat=True)) def get_language_order(self, language: Language) -> int: """Returns key suitable for ordering languages based on user preferences.""" if language.pk in self.primary_language_ids: return 0 if language.pk in self.secondary_language_ids: return 1 return 2 @cached_property def watched_project_ids(self): # We do not use values_list, because we prefetch this return {watched.id for watched in self.watched.all()} def watches_project(self, project): return project.id in self.watched_project_ids
class User(AbstractBaseUser): username = UsernameField( _("Username"), max_length=USERNAME_LENGTH, unique=True, help_text=_("Username may only contain letters, " "numbers or the following characters: @ . + - _"), validators=[validate_username], error_messages={ "unique": _("A user with that username already exists.") }, ) full_name = models.CharField( _("Full name"), max_length=FULLNAME_LENGTH, blank=False, validators=[validate_fullname], ) email = EmailField( # noqa: DJ01 _("E-mail"), blank=False, null=True, max_length=EMAIL_LENGTH, unique=True, validators=[validate_email], ) is_superuser = models.BooleanField( _("Superuser status"), default=False, help_text=_("User has all possible permissions."), ) is_active = models.BooleanField( _("Active"), default=True, help_text=_("Mark user as inactive instead of removing."), ) date_joined = models.DateTimeField(_("Date joined"), default=timezone.now) groups = GroupManyToManyField( Group, verbose_name=_("Groups"), blank=True, help_text=_("The user is granted all permissions included in " "membership of these groups."), ) objects = UserManager() EMAIL_FIELD = "email" USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email", "full_name"] DUMMY_FIELDS = ("first_name", "last_name", "is_staff") def __str__(self): return self.full_name def get_absolute_url(self): return reverse("user_page", kwargs={"user": self.username}) def save(self, *args, **kwargs): # Generate full name from parts # This is needed with LDAP authentication when the # server does not contain full name if "first_name" in self.extra_data and "last_name" in self.extra_data: self.full_name = "{first_name} {last_name}".format( **self.extra_data) elif "first_name" in self.extra_data: self.full_name = self.extra_data["first_name"] elif "last_name" in self.extra_data: self.full_name = self.extra_data["last_name"] if not self.email: self.email = None super().save(*args, **kwargs) self.clear_cache() def __init__(self, *args, **kwargs): self.extra_data = {} self.cla_cache = {} self._permissions = None self.current_subscription = None for name in self.DUMMY_FIELDS: if name in kwargs: self.extra_data[name] = kwargs.pop(name) super().__init__(*args, **kwargs) def clear_cache(self): self.cla_cache = {} self._permissions = None perm_caches = ( "project_permissions", "component_permissions", "allowed_projects", "allowed_project_ids", "watched_projects", "owned_projects", ) for name in perm_caches: if name in self.__dict__: del self.__dict__[name] def has_usable_password(self): # For some reason Django says that empty string is a valid password return self.password and super().has_usable_password() @cached_property def is_anonymous(self): return self.username == settings.ANONYMOUS_USER_NAME @cached_property def is_authenticated(self): return not self.is_anonymous def get_full_name(self): return self.full_name def get_short_name(self): return self.full_name def __setattr__(self, name, value): """Mimic first/last name for third party auth and ignore is_staff flag.""" if name in self.DUMMY_FIELDS: self.extra_data[name] = value else: super().__setattr__(name, value) def has_module_perms(self, module): """Compatibility API for admin interface.""" return self.is_superuser @property def is_staff(self): """Compatibility API for admin interface.""" return self.is_superuser @property def first_name(self): """Compatibility API for third party modules.""" return "" @property def last_name(self): """Compatibility API for third party modules.""" return self.full_name def has_perms(self, perm_list, obj=None): return all(self.has_perm(perm, obj) for perm in perm_list) # pylint: disable=keyword-arg-before-vararg def has_perm(self, perm, obj=None): """Permission check.""" # Weblate global scope permissions if perm in GLOBAL_PERM_NAMES: return check_global_permission(self, perm, obj) # Compatibility API for admin interface if obj is None: if not self.is_superuser: return False # Check permissions restrictions allowed = settings.AUTH_RESTRICT_ADMINS.get(self.username) return allowed is None or perm in allowed # Validate perms, this is expensive to perform, so this only in test by # default if settings.AUTH_VALIDATE_PERMS and ":" not in perm: try: Permission.objects.get(codename=perm) except Permission.DoesNotExist: raise ValueError("Invalid permission: {}".format(perm)) # Special permission functions if perm in SPECIALS: return SPECIALS[perm](self, perm, obj) # Generic permission return check_permission(self, perm, obj) def can_access_project(self, project): """Check access to given project.""" if self.is_superuser: return True return project.pk in self.project_permissions def check_access(self, project): """Raise an error if user is not allowed to access this project.""" if not self.can_access_project(project): raise Http404("Access denied") def can_access_component(self, component): """Check access to given component.""" if self.is_superuser: return True if not self.can_access_project(component.project): return False return not component.restricted or component.pk in self.component_permissions def check_access_component(self, component): """Raise an error if user is not allowed to access this component.""" if not self.can_access_component(component): raise Http404("Access denied") @cached_property def allowed_projects(self): """List of allowed projects.""" if self.is_superuser: return Project.objects.order() return Project.objects.filter(pk__in=self.allowed_project_ids) @cached_property def allowed_project_ids(self): """ Set with ids of allowed projects. This is more effective to use in queries than doing complex joins. """ if self.is_superuser: return set(Project.objects.values_list("id", flat=True)) return set(self.project_permissions.keys()) @cached_property def watched_projects(self): """ List of watched projects. Ensure ACL filtering applies (user could have been removed from the project meanwhile) """ return self.profile.watched.filter(id__in=self.allowed_project_ids) @cached_property def owned_projects(self): return self.projects_with_perm("project.edit") def _fetch_permissions(self): """Fetch all user permissions into a dictionary.""" projects = defaultdict(list) components = defaultdict(list) for group in self.groups.iterator(): languages = set( Group.languages.through.objects.filter( group=group).values_list("language_id", flat=True)) permissions = set( group.roles.values_list("permissions__codename", flat=True)) # Component list specific permissions componentlist_values = group.componentlists.values_list( "components__id", "components__project_id") if componentlist_values: for component, project in componentlist_values: components[component].append((permissions, languages)) # Grant access to the project projects[project].append(((), languages)) continue # Component specific permissions component_values = group.components.values_list("id", "project_id") if component_values: for component, project in component_values: components[component].append((permissions, languages)) # Grant access to the project projects[project].append(((), languages)) continue # Project specific permissions for project in Group.projects.through.objects.filter( group=group).values_list("project_id", flat=True): projects[project].append((permissions, languages)) self._permissions = {"projects": projects, "components": components} @cached_property def project_permissions(self): """Dictionary with all project permissions.""" if self._permissions is None: self._fetch_permissions() return self._permissions["projects"] @cached_property def component_permissions(self): """Dictionary with all project permissions.""" if self._permissions is None: self._fetch_permissions() return self._permissions["components"] def projects_with_perm(self, perm): if self.is_superuser: return Project.objects.all().order() groups = Group.objects.filter(user=self, roles__permissions__codename=perm) return Project.objects.filter(group__in=groups).distinct().order() def get_visible_name(self): # Get full name from database or username result = self.full_name or self.username return result.replace("<", "").replace(">", "").replace('"', "") def get_author_name(self, email=True): """Return formatted author name with e-mail.""" # The < > are replace to avoid tricking Git to use # name as e-mail full_name = self.get_visible_name() # Add e-mail if we are asked for it if not email: return full_name return "{0} <{1}>".format(full_name, self.email)