예제 #1
0
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
예제 #2
0
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
예제 #3
0
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)