diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cf39121 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DOMAIN=localhost +DEMO=false diff --git a/README.md b/README.md index 44a3151..375cd85 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ -# INSTALACION: +# INSTALACIÓN: -la instalacion es muy estandar +La instalación es muy estándar ``` - python -m venv env - source env/bin/actevate - python install -r requirements.txt +python -m venv env +source env/bin/actevate +python install -r requirements.txt ``` -## IMPORTANT EXTERNAL DEPENDETS +## IMPORTANT EXTERNAL DEPENDENCIES -Para arrancarlo es necesario tener el paquete xapian-bindings en tu ordenador. No se instala por pip asi que depende de cada sistema operativo: -https://xapian.org/download +Para arrancarlo es necesario tener el paquete `xapian-bindings` en tu ordenador. No se instala mediante `pip`, así que depende de cada [sistema operativo](https://xapian.org/download). Luego solo necesitas: ``` - ./manage.py migrate - ./manage.py runserver +./manage.py migrate +./manage.py runserver ``` diff --git a/admin/__init__.py b/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin/admin.py b/admin/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/admin/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/admin/apps.py b/admin/apps.py new file mode 100644 index 0000000..6d596f5 --- /dev/null +++ b/admin/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AdminConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "admin" diff --git a/admin/migrations/__init__.py b/admin/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin/models.py b/admin/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/admin/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/admin/templates/admin_panel.html b/admin/templates/admin_panel.html new file mode 100644 index 0000000..3dd5ed1 --- /dev/null +++ b/admin/templates/admin_panel.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ +
+
+ + {% translate "Institution" %} + +
+
+ +{% endblock %} diff --git a/admin/templates/admin_users.html b/admin/templates/admin_users.html new file mode 100644 index 0000000..37fbf96 --- /dev/null +++ b/admin/templates/admin_users.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ {% translate "Add new user" %} +
+
+ +
+
+ + + + + + + + + + + + {% for u in users %} + + + + + + {% endfor %} + +
Emailis Admin
{{ u.email }}{{ u.is_admin }}
+
+
+ +{% endblock %} diff --git a/admin/templates/delete_user.html b/admin/templates/delete_user.html new file mode 100644 index 0000000..5aa402d --- /dev/null +++ b/admin/templates/delete_user.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ +{% load django_bootstrap5 %} +
+
+ Are you sure than want remove the lot {{ object.name }} with {{ object.devices.count }} devices. +
+
+ +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} +
+ {% translate "Cancel" %} + +
+ +
+{% endblock %} diff --git a/admin/templates/institution.html b/admin/templates/institution.html new file mode 100644 index 0000000..88014b9 --- /dev/null +++ b/admin/templates/institution.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} +
+ {% translate "Cancel" %} + +
+ +
+{% endblock %} diff --git a/admin/templates/user.html b/admin/templates/user.html new file mode 100644 index 0000000..1ea00d8 --- /dev/null +++ b/admin/templates/user.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} +
+ {% translate "Cancel" %} + +
+ +
+{% endblock %} diff --git a/admin/tests.py b/admin/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/admin/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/admin/urls.py b/admin/urls.py new file mode 100644 index 0000000..9a26cbf --- /dev/null +++ b/admin/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from admin import views + +app_name = 'admin' + +urlpatterns = [ + path("panel/", views.PanelView.as_view(), name="panel"), + path("users/", views.UsersView.as_view(), name="users"), + path("users/new", views.CreateUserView.as_view(), name="new_user"), + path("users/edit/", views.EditUserView.as_view(), name="edit_user"), + path("users/delete/", views.DeleteUserView.as_view(), name="delete_user"), + path("institution/", views.InstitutionView.as_view(), name="institution"), +] diff --git a/admin/views.py b/admin/views.py new file mode 100644 index 0000000..9ce25c7 --- /dev/null +++ b/admin/views.py @@ -0,0 +1,126 @@ +from smtplib import SMTPException +from django.urls import reverse_lazy +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ +from django.views.generic.base import TemplateView +from django.views.generic.edit import ( + CreateView, + UpdateView, + DeleteView, +) +from dashboard.mixins import DashboardView, Http403 +from user.models import User, Institution +from admin.email import NotifyActivateUserByEmail + + +class AdminView(DashboardView): + def get(self, *args, **kwargs): + response = super().get(*args, **kwargs) + if not self.request.user.is_admin: + raise Http403 + + return response + +class PanelView(AdminView, TemplateView): + template_name = "admin_panel.html" + title = _("Admin") + breadcrumb = _("admin") + " /" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + return context + + +class UsersView(AdminView, TemplateView): + template_name = "admin_users.html" + title = _("Users") + breadcrumb = _("admin / Users") + " /" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + "users": User.objects.filter() + }) + return context + + +class CreateUserView(AdminView, NotifyActivateUserByEmail, CreateView): + template_name = "user.html" + title = _("User") + breadcrumb = _("admin / User") + " /" + success_url = reverse_lazy('admin:users') + model = User + fields = ( + "email", + "password", + "is_admin", + ) + + def form_valid(self, form): + form.instance.institution = self.request.user.institution + form.instance.set_password(form.instance.password) + response = super().form_valid(form) + + try: + self.send_email(form.instance) + except SMTPException as e: + messages.error(self.request, e) + + return response + + +class DeleteUserView(AdminView, DeleteView): + template_name = "delete_user.html" + title = _("Delete user") + breadcrumb = "admin / Delete user" + success_url = reverse_lazy('admin:users') + model = User + fields = ( + "email", + "password", + "is_admin", + ) + + def form_valid(self, form): + response = super().form_valid(form) + return response + + +class EditUserView(AdminView, UpdateView): + template_name = "user.html" + title = _("Edit user") + breadcrumb = "admin / Edit user" + success_url = reverse_lazy('admin:users') + model = User + fields = ( + "email", + "is_admin", + ) + + def get_form_kwargs(self): + pk = self.kwargs.get('pk') + self.object = get_object_or_404(self.model, pk=pk) + #self.object.set_password(self.object.password) + kwargs = super().get_form_kwargs() + return kwargs + + +class InstitutionView(AdminView, UpdateView): + template_name = "institution.html" + title = _("Edit institution") + section = "admin" + subtitle = _('Edit institution') + model = Institution + success_url = reverse_lazy('admin:panel') + fields = ( + "name", + "logo", + "location", + "responsable_person", + "supervisor_person" + ) + + def get_form_kwargs(self): + self.object = self.request.user.institution + kwargs = super().get_form_kwargs() + return kwargs diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 22c5a1f..b15c44e 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.6 on 2024-09-19 15:09 +# Generated by Django 5.0.6 on 2024-10-10 11:34 import django.db.models.deletion from django.conf import settings @@ -26,6 +26,7 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("tag", models.CharField(max_length=50)), ("token", models.UUIDField()), ( "owner", diff --git a/api/models.py b/api/models.py index b8bbc24..8cdf430 100644 --- a/api/models.py +++ b/api/models.py @@ -1,9 +1,8 @@ from django.db import models from user.models import User -# Create your models here. - class Token(models.Model): + tag = models.CharField(max_length=50) token = models.UUIDField() owner = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/api/tables.py b/api/tables.py index ac1cc7a..27c8cbf 100644 --- a/api/tables.py +++ b/api/tables.py @@ -33,12 +33,30 @@ class TokensTable(tables.Table): }, orderable=False ) - + edit_token = ButtonColumn( + linkify={ + "viewname": "api:edit_token", + "args": [tables.A("pk")] + }, + attrs = { + "a": { + "type": "button", + "class": "text-primary", + "title": "Remove", + } + }, + orderable=False, + verbose_name="Edit" + ) token = tables.Column(verbose_name=_("Token"), empty_values=()) + tag = tables.Column(verbose_name=_("Tag"), empty_values=()) def render_view_user(self): return format_html('') + def render_edit_token(self): + return format_html('') + # def render_token(self, record): # return record.get_memberships() @@ -63,5 +81,5 @@ class TokensTable(tables.Table): class Meta: model = Token template_name = "custom_table.html" - fields = ("token", "view_user") + fields = ("token", "tag", "edit_token") diff --git a/api/templates/new_token.html b/api/templates/new_token.html new file mode 100644 index 0000000..77e0503 --- /dev/null +++ b/api/templates/new_token.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} +
+
+

{{ subtitle }}

+
+
+ +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} + + +
+{% endblock %} diff --git a/api/templates/token.html b/api/templates/token.html index 5185090..2ba2ca7 100644 --- a/api/templates/token.html +++ b/api/templates/token.html @@ -8,6 +8,7 @@ {{ subtitle }} {% render_table table %} + diff --git a/api/urls.py b/api/urls.py index f3c8028..5eac279 100644 --- a/api/urls.py +++ b/api/urls.py @@ -9,5 +9,6 @@ urlpatterns = [ path('snapshot/', views.NewSnapshot, name='new_snapshot'), path('tokens/', views.TokenView.as_view(), name='tokens'), path('tokens/new', views.TokenNewView.as_view(), name='new_token'), + path("tokens//edit", views.EditTokenView.as_view(), name="edit_token"), path('tokens//del', views.TokenDeleteView.as_view(), name='delete_token'), ] diff --git a/api/views.py b/api/views.py index fbccb79..175fe3e 100644 --- a/api/views.py +++ b/api/views.py @@ -1,13 +1,19 @@ import json +from django.conf import settings +from django.urls import reverse_lazy from django.shortcuts import get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from django.core.exceptions import ValidationError -from django.views.generic.edit import DeleteView -from django.views.generic.base import View -from django.http import JsonResponse from django_tables2 import SingleTableView +from django.views.generic.base import View +from django.views.generic.edit import ( + CreateView, + DeleteView, + UpdateView, +) +from django.http import JsonResponse from uuid import uuid4 from dashboard.mixins import DashboardView @@ -29,14 +35,14 @@ def NewSnapshot(request): return JsonResponse({'error': 'Invalid request method'}, status=400) # Authentication - # auth_header = request.headers.get('Authorization') - # if not auth_header or not auth_header.startswith('Bearer '): - # return JsonResponse({'error': 'Invalid or missing token'}, status=401) + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return JsonResponse({'error': 'Invalid or missing token'}, status=401) - # token = auth_header.split(' ')[1] - # tk = Token.objects.filter(token=token).first() - # if not tk: - # return JsonResponse({'error': 'Invalid or missing token'}, status=401) + token = auth_header.split(' ')[1] + tk = Token.objects.filter(token=token).first() + if not tk: + return JsonResponse({'error': 'Invalid or missing token'}, status=401) # Validation snapshot try: @@ -60,15 +66,34 @@ def NewSnapshot(request): # save_in_disk(data, tk.user) try: - # Build(data, tk.user) - user = User.objects.get(email="user@example.org") - Build(data, user) + Build(data, tk.owner) except Exception: return JsonResponse({'status': 'fail'}, status=200) - return JsonResponse({'status': 'success'}, status=200) + annotation = Annotation.objects.filter( + uuid=data['uuid'], + type=Annotation.Type.SYSTEM, + # TODO this is hardcoded, it should select the user preferred algorithm + key="hidalgo1", + owner=tk.owner.institution + ).first() + if not annotation: + return JsonResponse({'status': 'fail'}, status=200) + + url = "{}://{}{}".format( + request.scheme, + settings.DOMAIN, + reverse_lazy("device:details", args=(annotation.value,)) + ) + response = { + "status": "success", + "dhid": annotation.value[:6].upper(), + "url": url, + "public_url": url + } + return JsonResponse(response, status=200) class TokenView(DashboardView, SingleTableView): @@ -89,7 +114,7 @@ class TokenView(DashboardView, SingleTableView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'tokens': Token.objects, + 'tokens': Token.objects.all(), }) return context @@ -105,10 +130,42 @@ class TokenDeleteView(DashboardView, DeleteView): return redirect('api:tokens') -class TokenNewView(DashboardView, View): +class TokenNewView(DashboardView, CreateView): + template_name = "new_token.html" + title = _("Credential management") + section = "Credential" + subtitle = _('New Tokens') + icon = 'bi bi-key' + model = Token + success_url = reverse_lazy('api:tokens') + fields = ( + "tag", + ) - def get(self, request, *args, **kwargs): - Token.objects.create(token=uuid4(), owner=self.request.user) + def form_valid(self, form): + form.instance.owner = self.request.user + form.instance.token = uuid4() + return super().form_valid(form) - return redirect('api:tokens') - + +class EditTokenView(DashboardView, UpdateView): + template_name = "new_token.html" + title = _("Credential management") + section = "Credential" + subtitle = _('New Tokens') + icon = 'bi bi-key' + model = Token + success_url = reverse_lazy('api:tokens') + fields = ( + "tag", + ) + + def get_form_kwargs(self): + pk = self.kwargs.get('pk') + self.object = get_object_or_404( + self.model, + owner=self.request.user, + pk=pk, + ) + kwargs = super().get_form_kwargs() + return kwargs diff --git a/dashboard/mixins.py b/dashboard/mixins.py index 7e2e236..aa63d36 100644 --- a/dashboard/mixins.py +++ b/dashboard/mixins.py @@ -1,5 +1,5 @@ from django.urls import resolve -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import get_object_or_404, redirect, Http404 from django.utils.translation import gettext_lazy as _ from django.core.exceptions import PermissionDenied from django.contrib.auth.mixins import LoginRequiredMixin @@ -45,10 +45,11 @@ class DashboardView(LoginRequiredMixin): def get_session_devices(self): dev_ids = self.request.session.pop("devices", []) - + self._devices = [] - annotation = Annotation.objects.filter(value__in=dev_ids) - for x in annotation.filter(owner=self.request.user.institution).distinct(): + for x in Annotation.objects.filter(value__in=dev_ids).filter( + owner=self.request.user.institution + ).distinct(): self._devices.append(Device(id=x.value)) return self._devices @@ -57,7 +58,11 @@ class DetailsMixin(DashboardView, TemplateView): def get(self, request, *args, **kwargs): self.pk = kwargs['pk'] - self.object = get_object_or_404(self.model, pk=self.pk, owner=self.request.user.institution) + self.object = get_object_or_404( + self.model, + pk=self.pk, + owner=self.request.user.institution + ) return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -71,14 +76,46 @@ class DetailsMixin(DashboardView, TemplateView): class InventaryMixin(DashboardView, TemplateView): def post(self, request, *args, **kwargs): - dev_ids = dict(self.request.POST).get("devices", []) - self.request.session["devices"] = dev_ids - url = self.request.POST.get("url") + post = dict(self.request.POST) + url = post.get("url") + if url: + dev_ids = post.get("devices", []) + self.request.session["devices"] = dev_ids + try: - resource = resolve(url) + resource = resolve(url[0]) if resource and dev_ids: - return redirect(url) + return redirect(url[0]) except Exception: pass return super().get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + limit = self.request.GET.get("limit") + page = self.request.GET.get("page") + try: + limit = int(limit) + page = int(page) + if page < 1: + page = 1 + if limit < 1: + limit = 10 + except: + limit = 10 + page = 1 + + offset = (page - 1) * limit + devices, count = self.get_devices(self.request.user, offset, limit) + total_pages = (count + limit - 1) // limit + + context.update({ + 'devices': devices, + 'count': count, + "limit": limit, + "offset": offset, + "page": page, + "total_pages": total_pages, + }) + return context diff --git a/dashboard/templates/base.html b/dashboard/templates/base.html index 318c890..40e6658 100644 --- a/dashboard/templates/base.html +++ b/dashboard/templates/base.html @@ -61,12 +61,12 @@