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")