diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2bcd70e --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 88 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..4e8191e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +#- repo: https://github.com/pre-commit/pre-commit-hooks + #rev: v3.2.0 + #hooks: + #- id: trailing-whitespace + #- id: end-of-file-fixer + #- id: check-yaml + #- id: check-added-large-files + +- repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + exclude: 'migrations|^shynet/shynet/settings.py' diff --git a/pyproject.toml b/pyproject.toml index d151001..41cfa0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,3 +39,6 @@ mypy = "^0.910" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 88 diff --git a/shynet/analytics/models.py b/shynet/analytics/models.py index d626f25..2200c19 100644 --- a/shynet/analytics/models.py +++ b/shynet/analytics/models.py @@ -15,27 +15,26 @@ def _default_uuid(): class Session(models.Model): uuid = models.UUIDField(default=_default_uuid, primary_key=True) service = models.ForeignKey( - Service, verbose_name=_('Service'), - on_delete=models.CASCADE, db_index=True + Service, verbose_name=_("Service"), on_delete=models.CASCADE, db_index=True ) # Cross-session identification; optional, and provided by the service identifier = models.TextField( - blank=True, db_index=True, verbose_name=_('Identifier') + blank=True, db_index=True, verbose_name=_("Identifier") ) # Time start_time = models.DateTimeField( - default=timezone.now, db_index=True, verbose_name=_('Start time') + default=timezone.now, db_index=True, verbose_name=_("Start time") ) last_seen = models.DateTimeField( - default=timezone.now, db_index=True, verbose_name=_('Last seen') + default=timezone.now, db_index=True, verbose_name=_("Last seen") ) # Core request information - user_agent = models.TextField(verbose_name=_('User agent')) - browser = models.TextField(verbose_name=_('Browser')) - device = models.TextField(verbose_name=_('Device')) + user_agent = models.TextField(verbose_name=_("User agent")) + browser = models.TextField(verbose_name=_("Browser")) + device = models.TextField(verbose_name=_("Device")) device_type = models.CharField( max_length=7, choices=[ @@ -46,23 +45,25 @@ class Session(models.Model): ("OTHER", _("Other")), ], default="OTHER", - verbose_name=_('Device type') + verbose_name=_("Device type"), ) - os = models.TextField(verbose_name=_('OS')) - ip = models.GenericIPAddressField(db_index=True, null=True, verbose_name=_('IP')) + os = models.TextField(verbose_name=_("OS")) + ip = models.GenericIPAddressField(db_index=True, null=True, verbose_name=_("IP")) # GeoIP data - asn = models.TextField(blank=True, verbose_name=_('Asn')) - country = models.TextField(blank=True, verbose_name=_('Country')) - longitude = models.FloatField(null=True, verbose_name=_('Longitude')) - latitude = models.FloatField(null=True, verbose_name=_('Latitude')) - time_zone = models.TextField(blank=True, verbose_name=_('Time zone')) + asn = models.TextField(blank=True, verbose_name=_("Asn")) + country = models.TextField(blank=True, verbose_name=_("Country")) + longitude = models.FloatField(null=True, verbose_name=_("Longitude")) + latitude = models.FloatField(null=True, verbose_name=_("Latitude")) + time_zone = models.TextField(blank=True, verbose_name=_("Time zone")) - is_bounce = models.BooleanField(default=True, db_index=True, verbose_name=_('Is bounce')) + is_bounce = models.BooleanField( + default=True, db_index=True, verbose_name=_("Is bounce") + ) class Meta: - verbose_name = _('Session') - verbose_name_plural = _('Sessions') + verbose_name = _("Session") + verbose_name_plural = _("Sessions") ordering = ["-start_time"] indexes = [ models.Index(fields=["service", "-start_time"]), @@ -96,8 +97,7 @@ class Session(models.Model): class Hit(models.Model): session = models.ForeignKey( - Session, on_delete=models.CASCADE, db_index=True, - verbose_name=_('Session') + Session, on_delete=models.CASCADE, db_index=True, verbose_name=_("Session") ) initial = models.BooleanField(default=True, db_index=True) @@ -119,8 +119,8 @@ class Hit(models.Model): service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True) class Meta: - verbose_name = _('Hit') - verbose_name_plural = _('Hits') + verbose_name = _("Hit") + verbose_name_plural = _("Hits") ordering = ["-start_time"] indexes = [ models.Index(fields=["session", "-start_time"]), @@ -129,7 +129,6 @@ class Hit(models.Model): models.Index(fields=["session", "referrer"]), ] - @property def duration(self): return self.last_seen - self.start_time diff --git a/shynet/api/apps.py b/shynet/api/apps.py index 66656fd..878e7d5 100644 --- a/shynet/api/apps.py +++ b/shynet/api/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ApiConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'api' + default_auto_field = "django.db.models.BigAutoField" + name = "api" diff --git a/shynet/api/mixins.py b/shynet/api/mixins.py index 47dc9a8..f6c8dba 100644 --- a/shynet/api/mixins.py +++ b/shynet/api/mixins.py @@ -6,11 +6,11 @@ from core.models import User class ApiTokenRequiredMixin: def _get_user_by_token(self, request): - token = request.headers.get('Authorization') - if not token or not token.startswith('Token '): + token = request.headers.get("Authorization") + if not token or not token.startswith("Token "): return AnonymousUser() - token = token.split(' ')[1] + token = token.split(" ")[1] user = User.objects.filter(api_token=token).first() return user if user else AnonymousUser() diff --git a/shynet/api/views.py b/shynet/api/views.py index a5dc895..44ef6dc 100644 --- a/shynet/api/views.py +++ b/shynet/api/views.py @@ -24,7 +24,7 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): Q(owner=request.user) | Q(collaborators__in=[request.user]) ).distinct() - uuid = request.GET.get('uuid') + uuid = request.GET.get("uuid") if uuid and is_valid_uuid(uuid): services = services.filter(uuid=uuid) @@ -32,29 +32,29 @@ class DashboardApiView(ApiTokenRequiredMixin, DateRangeMixin, View): start = self.get_start_date() end = self.get_end_date() except ValueError: - return JsonResponse(status=400, data={'error': 'Invalid date format'}) + return JsonResponse(status=400, data={"error": "Invalid date format"}) services_data = [ { - 'name': s.name, - 'uuid': s.uuid, - 'link': s.link, - 'stats': s.get_core_stats(start, end), + "name": s.name, + "uuid": s.uuid, + "link": s.link, + "stats": s.get_core_stats(start, end), } for s in services ] services_data = self._convert_querysets_to_lists(services_data) - return JsonResponse(data={'services': services_data}) + return JsonResponse(data={"services": services_data}) def _convert_querysets_to_lists(self, services_data): for service_data in services_data: - for key, value in service_data['stats'].items(): + for key, value in service_data["stats"].items(): if isinstance(value, QuerySet): - service_data['stats'][key] = list(value) - for key, value in service_data['stats']['compare'].items(): + service_data["stats"][key] = list(value) + for key, value in service_data["stats"]["compare"].items(): if isinstance(value, QuerySet): - service_data['stats']['compare'][key] = list(value) + service_data["stats"]["compare"][key] = list(value) return services_data diff --git a/shynet/core/models.py b/shynet/core/models.py index 6fc01d3..7d6530a 100644 --- a/shynet/core/models.py +++ b/shynet/core/models.py @@ -64,38 +64,51 @@ class Service(models.Model): SERVICE_STATUSES = [(ACTIVE, _("Active")), (ARCHIVED, _("Archived"))] uuid = models.UUIDField(default=_default_uuid, primary_key=True) - name = models.TextField(max_length=64, verbose_name=_('Name')) + name = models.TextField(max_length=64, verbose_name=_("Name")) owner = models.ForeignKey( - User, verbose_name=_('Owner'), - on_delete=models.CASCADE, related_name="owning_services" + User, + verbose_name=_("Owner"), + on_delete=models.CASCADE, + related_name="owning_services", ) collaborators = models.ManyToManyField( - User, verbose_name=_('Collaborators'), - related_name="collaborating_services", blank=True + User, + verbose_name=_("Collaborators"), + related_name="collaborating_services", + blank=True, ) - created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) - link = models.URLField(blank=True, verbose_name=_('link')) - origins = models.TextField(default="*", verbose_name=_('origins')) + created = models.DateTimeField(auto_now_add=True, verbose_name=_("created")) + link = models.URLField(blank=True, verbose_name=_("link")) + origins = models.TextField(default="*", verbose_name=_("origins")) status = models.CharField( - max_length=2, choices=SERVICE_STATUSES, default=ACTIVE, db_index=True, - verbose_name=_('status') + max_length=2, + choices=SERVICE_STATUSES, + default=ACTIVE, + db_index=True, + verbose_name=_("status"), ) - respect_dnt = models.BooleanField(default=True, verbose_name=_('Respect dnt')) - ignore_robots = models.BooleanField(default=False, verbose_name=_('Ignore robots')) - collect_ips = models.BooleanField(default=True, verbose_name=_('Collect ips')) + respect_dnt = models.BooleanField(default=True, verbose_name=_("Respect dnt")) + ignore_robots = models.BooleanField(default=False, verbose_name=_("Ignore robots")) + collect_ips = models.BooleanField(default=True, verbose_name=_("Collect ips")) ignored_ips = models.TextField( - default="", blank=True, validators=[_validate_network_list], - verbose_name=_('Igored ips') + default="", + blank=True, + validators=[_validate_network_list], + verbose_name=_("Igored ips"), ) hide_referrer_regex = models.TextField( - default="", blank=True, validators=[_validate_regex], - verbose_name=_('Hide referrer regex') + default="", + blank=True, + validators=[_validate_regex], + verbose_name=_("Hide referrer regex"), + ) + script_inject = models.TextField( + default="", blank=True, verbose_name=_("Script inject") ) - script_inject = models.TextField(default="", blank=True, verbose_name=_('Script inject')) class Meta: - verbose_name = _('Service') - verbose_name_plural = _('Services') + verbose_name = _("Service") + verbose_name_plural = _("Services") ordering = ["name", "uuid"] def __str__(self): diff --git a/shynet/dashboard/forms.py b/shynet/dashboard/forms.py index e6a2974..d8fc249 100644 --- a/shynet/dashboard/forms.py +++ b/shynet/dashboard/forms.py @@ -25,9 +25,15 @@ class ServiceForm(forms.ModelForm): "name": forms.TextInput(), "origins": forms.TextInput(), "ignored_ips": forms.TextInput(), - "respect_dnt": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]), - "collect_ips": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]), - "ignore_robots": forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]), + "respect_dnt": forms.RadioSelect( + choices=[(True, _("Yes")), (False, _("No"))] + ), + "collect_ips": forms.RadioSelect( + choices=[(True, _("Yes")), (False, _("No"))] + ), + "ignore_robots": forms.RadioSelect( + choices=[(True, _("Yes")), (False, _("No"))] + ), "hide_referrer_regex": forms.TextInput(), "script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}), } @@ -45,17 +51,29 @@ class ServiceForm(forms.ModelForm): "origins": _( "At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)." ), - "respect_dnt": _("Should visitors who have enabled Do Not Track be excluded from all data?"), - "ignored_ips": _("A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')."), - "ignore_robots": _("Should sessions generated by bots be excluded from tracking?"), - "hide_referrer_regex": _("Any referrers that match this RegEx will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank."), - "script_inject": _("Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed."), + "respect_dnt": _( + "Should visitors who have enabled Do Not Track be excluded from all data?" + ), + "ignored_ips": _( + "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32')." + ), + "ignore_robots": _( + "Should sessions generated by bots be excluded from tracking?" + ), + "hide_referrer_regex": _( + "Any referrers that match this RegEx will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank." + ), + "script_inject": _( + "Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed." + ), } collect_ips = forms.BooleanField( help_text=_("IP address collection is disabled globally by your administrator.") if settings.BLOCK_ALL_IPS - else _("Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected."), + else _( + "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected." + ), widget=forms.RadioSelect(choices=[(True, _("Yes")), (False, _("No"))]), initial=False if settings.BLOCK_ALL_IPS else True, required=False, @@ -68,7 +86,9 @@ class ServiceForm(forms.ModelForm): return False if settings.BLOCK_ALL_IPS else collect_ips collaborators = forms.CharField( - help_text=_("Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)"), + help_text=_( + "Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)" + ), required=False, ) diff --git a/shynet/dashboard/views.py b/shynet/dashboard/views.py index 97360d0..ebec65f 100644 --- a/shynet/dashboard/views.py +++ b/shynet/dashboard/views.py @@ -161,4 +161,4 @@ class RefreshApiTokenView(LoginRequiredMixin, View): def post(self, request): request.user.api_token = _default_api_token() request.user.save() - return redirect('account_change_password') + return redirect("account_change_password")