diff --git a/backend/core/analysis/migrations/0019_remove_dockerprocess_file_to_analyse_and_more.py b/backend/core/analysis/migrations/0019_remove_dockerprocess_file_to_analyse_and_more.py new file mode 100644 index 00000000..c2d39c39 --- /dev/null +++ b/backend/core/analysis/migrations/0019_remove_dockerprocess_file_to_analyse_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.2 on 2023-10-19 12:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_analysis', '0018_alter_dockerprocess_resources'), + ] + + operations = [ + migrations.RemoveField( + model_name='dockerprocess', + name='file_to_analyse', + ), + migrations.RemoveField( + model_name='dockerprocess', + name='owner', + ), + migrations.DeleteModel( + name='Analysis', + ), + migrations.DeleteModel( + name='DockerProcess', + ), + ] diff --git a/backend/core/fileupload/migrations/0012_alter_license_label.py b/backend/core/fileupload/migrations/0012_alter_license_label.py new file mode 100644 index 00000000..23f87a20 --- /dev/null +++ b/backend/core/fileupload/migrations/0012_alter_license_label.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-10-19 12:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_fileupload', '0011_analysisresult'), + ] + + operations = [ + migrations.AlterField( + model_name='license', + name='label', + field=models.TextField(default='CC BY - SA 4.0 DEED'), + ), + ] diff --git a/backend/core/fileupload/migrations/0013_file_private_alter_file_family_alter_file_version.py b/backend/core/fileupload/migrations/0013_file_private_alter_file_family_alter_file_version.py new file mode 100644 index 00000000..26bbacdf --- /dev/null +++ b/backend/core/fileupload/migrations/0013_file_private_alter_file_family_alter_file_version.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.2 on 2023-10-23 13:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_fileupload', '0012_alter_license_label'), + ] + + operations = [ + migrations.AlterField( + model_name='file', + name='family', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core_fileupload.family'), + ), + migrations.AlterField( + model_name='file', + name='version', + field=models.CharField(blank=True, max_length=16, null=True), + ), + ] diff --git a/backend/core/fileupload/migrations/0014_file_private.py b/backend/core/fileupload/migrations/0014_file_private.py new file mode 100644 index 00000000..bb4070f8 --- /dev/null +++ b/backend/core/fileupload/migrations/0014_file_private.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.2 on 2023-10-26 07:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_fileupload', '0013_file_private_alter_file_family_alter_file_version'), + ] + + operations = [ + migrations.AddField( + model_name='file', + name='private', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/core/fileupload/models.py b/backend/core/fileupload/models.py index 3816bc31..0bf48814 100644 --- a/backend/core/fileupload/models.py +++ b/backend/core/fileupload/models.py @@ -1,4 +1,7 @@ from django.db import models +from django.db.models.signals import post_migrate +from django.dispatch import receiver + from core.user.models import User from django.core.files.base import ContentFile from django.template.defaultfilters import slugify # new @@ -67,6 +70,13 @@ def create(self, **kwargs): lic.save() return lic + def create_default_license(self): + """ + Creates the default license. + """ + default_license_label = "CC BY - SA 4.0 DEED" + default_license, created = self.get_or_create(label=default_license_label) + return default_license class License(models.Model): """ @@ -74,7 +84,7 @@ class License(models.Model): """ objects = LicenseManager() - _default_license = "CC BY - Mention" + _default_license = "CC BY - SA 4.0 DEED" label = models.TextField(blank=False, default=_default_license) @@ -82,6 +92,13 @@ def __str__(self): # do not change that return f"{self.id}" + @receiver(post_migrate) + def create_default_license(sender, **kwargs): + """ + Creates the default license after migrations. + """ + License.objects.create_default_license() + # -------------------------------------------------- File Model -------------------------------------------------- class FileManager(models.Manager): @@ -101,13 +118,14 @@ def save_file(self, local_file, **kwargs): if tags is None: raise TypeError("Tags is not set") family = kwargs.get("family", None) - # get file from id - if kwargs.get("version", None) is None: - raise TypeError("Version is not set") + version = kwargs.get("version", None) + private = kwargs.get("private", None) # get license from id - if kwargs.get("license", None) is None: + if kwargs.get("license", None) is None and kwargs.get("private") is None: raise TypeError("License not set!") + file = self.model(**kwargs) + file.save() file.tags.set(tags) file.save() @@ -140,14 +158,14 @@ class File(models.Model): relative_upload_dir = "files/" owner = models.ForeignKey(User, on_delete=models.RESTRICT) - family = models.ForeignKey(Family, on_delete=models.CASCADE) + family = models.ForeignKey(Family, on_delete=models.CASCADE, null=True, blank=True) label = models.CharField(blank=False, max_length=255) description = models.TextField(blank=True) local_file = models.FileField(upload_to=relative_upload_dir) uploaded_at = models.DateTimeField(auto_now_add=True) license = models.ForeignKey(License, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag) - version = models.CharField(blank=False, null=False, max_length=16) + version = models.CharField(blank=True, null=True, max_length=16) transpiled_file = models.FileField( null=True, blank=True, upload_to=relative_upload_dir ) @@ -159,6 +177,9 @@ class File(models.Model): ) # indicates if the user confirmed the upload slug = models.SlugField(null=True) confirmation_token = models.CharField(default="", max_length=255) + private = models.BooleanField( + default=False + ) def __str__(self): # do not change that diff --git a/backend/core/fileupload/serializers.py b/backend/core/fileupload/serializers.py index 091a98bb..cae58368 100644 --- a/backend/core/fileupload/serializers.py +++ b/backend/core/fileupload/serializers.py @@ -62,6 +62,9 @@ def validate(self, data): """ Check that the uploaded file contains valid xml """ + family = data.get('family', None) + if family is None: + data['family'] = None try: contents = "" for line in data['local_file']: @@ -108,7 +111,8 @@ def to_internal_value(self, data): tags = list(map(lambda tag: Tag.objects.get(id=tag), data.getlist('tags'))) - if 'family' in data: + family = None + if 'family' in data and data['family']: family = Family.objects.get(id=data['family']) license = License.objects.get(id=data['license']) diff --git a/backend/core/fileupload/tests.py b/backend/core/fileupload/tests.py index 68df92fa..abc39754 100644 --- a/backend/core/fileupload/tests.py +++ b/backend/core/fileupload/tests.py @@ -522,21 +522,21 @@ def test_license_list_logged_in_admin(self): self.client.force_authenticate(self.admin) res = self.client.get("/licenses/") json = res.json() - self.assertEqual(len(json), 2) + self.assertEqual(len(json), 3) def test_license_list_logged_in_user(self): # Licenses are listable when logged in with non-admin user self.client.force_authenticate(self.user) res = self.client.get("/licenses/") json = res.json() - self.assertEqual(len(json), 2) + self.assertEqual(len(json), 3) def test_license_list_logged_out(self): # Licenses are listable when logged out self.client.force_authenticate(None) res = self.client.get("/licenses/") json = res.json() - self.assertEqual(len(json), 2) + self.assertEqual(len(json), 3) def test_license_retrieve_logged_in_admin(self): # License is retrievable when logged in diff --git a/backend/core/fileupload/viewsets.py b/backend/core/fileupload/viewsets.py index 0b6c32ab..4e7cf4a1 100644 --- a/backend/core/fileupload/viewsets.py +++ b/backend/core/fileupload/viewsets.py @@ -69,8 +69,11 @@ def anonymize_file(file, request): tags.append(new_tag) anonymized_file[file_key] = tags elif file_key == "family": - new_family = file_value - new_family.update({"owner": new_family["owner"] == user_email}) + if file_value is not None: + new_family = file_value.copy() + new_family.update({"owner": new_family.get("owner") == user_email}) + else: + new_family = None anonymized_file[file_key] = new_family else: anonymized_file[file_key] = file_value @@ -172,8 +175,10 @@ class UploadApiView(APIView): permission_classes = [permissions.IsAuthenticated] def check_family(self, request, family_id): - family = Family.objects.get(pk=family_id) - return family and family.owner == request.user + if family_id: + family = Family.objects.get(pk=family_id) + return family and family.owner == request.user + return True def check_tags(self, request, tag_ids): for tag_id in tag_ids: @@ -198,21 +203,72 @@ def schedule_analysis(self, fs): analysis_result.save() -class BulkUploadApiView(UploadApiView): +class PrivateFileUploadApiView(APIView): + permission_classes = [permissions.IsAuthenticated] + def post(self, request, format=None): if not request.data["files"]: return Response( - {"files": "This field may not be blank."}, - status.HTTP_400_BAD_REQUEST, - ) + {"files": "This field may not be blank."}, + status.HTTP_400_BAD_REQUEST, + ) try: files = json.loads(request.data["files"]) except: return Response( - {"files": "This field must contain JSON."}, - status.HTTP_400_BAD_REQUEST, + {"files": "This field must contain JSON."}, + status.HTTP_400_BAD_REQUEST, + ) + for uploaded_file in files: + label = uploaded_file["label"] + description = uploaded_file["description"] + file = request.FILES[uploaded_file["file"]] + license = uploaded_file["license"] + tags = uploaded_file["tags"] + + if not label or not file: + return Response( + {"message": "Missing required fields"}, + status=status.HTTP_400_BAD_REQUEST, ) + validated_data = QueryDict("", mutable=True) + validated_data["label"] = label + validated_data["description"] = description + validated_data.setlist("tags", tags) + validated_data["local_file"] = file + validated_data["private"] = True + validated_data["license"] = license + + fs = FilesSerializer(data=validated_data) + if fs.is_valid(): + fs.save(owner=request.user) + return Response( + {"message": "File uploaded successfully"}, + status=status.HTTP_201_CREATED, + ) + else: + return Response( + {"message": "Invalid file format"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + +class BulkUploadApiView(UploadApiView): + def post(self, request, format=None): + if not request.data["files"]: + return Response( + {"files": "This field may not be blank."}, + status.HTTP_400_BAD_REQUEST, + ) + try: + files = json.loads(request.data["files"]) + except: + return Response( + {"files": "This field must contain JSON."}, + status.HTTP_400_BAD_REQUEST, + ) + serializers = [] confirmation_token = generate_random_string(30) @@ -264,16 +320,16 @@ class ZipUploadApiView(UploadApiView): def post(self, request, format=None): if not request.data["files"]: return Response( - {"files": "This field may not be blank."}, - status.HTTP_400_BAD_REQUEST, - ) + {"files": "This field may not be blank."}, + status.HTTP_400_BAD_REQUEST, + ) try: file_data = json.loads(request.data["files"]) except: return Response( - {"files": "This field must contain JSON."}, - status.HTTP_400_BAD_REQUEST, - ) + {"files": "This field must contain JSON."}, + status.HTTP_400_BAD_REQUEST, + ) label = file_data["label"] description = file_data["description"] @@ -299,7 +355,7 @@ def post(self, request, format=None): validated_data["label"] = label validated_data["description"] = description validated_data["license"] = license - validated_data["version"] = f"{i+1}.0.0" + validated_data["version"] = f"{i + 1}.0.0" validated_data["family"] = family validated_data.setlist("tags", tags) validated_data["local_file"] = local_file @@ -405,6 +461,29 @@ def retrieve(self, request, *args, **kwargs): return Response(anonymized_file) +class PrivateFileViewSet(viewsets.ModelViewSet): + queryset = File.objects.filter(private=True) + serializer_class = FilesSerializer + permission_classes = [IsAuthenticatedOrReadOnly, IsOwnerOrIsAdminOrReadOnly] + + def list(self, request, *args, **kwargs): + queryset = self.queryset.filter(owner=request.user) + serializer = self.serializer_class(queryset, many=True) + files = serializer.data + anonymized_files = [] + for file in files: + anonymized_file = anonymize_file(file, request) + anonymized_files.append(anonymized_file) + return Response(anonymized_files) + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + if instance.owner != request.user: + return Response({"error": "Access denied"}, status=status.HTTP_403_FORBIDDEN) + serializer = self.serializer_class(instance) + return Response(serializer.data) + + class FamiliesViewSet( viewsets.GenericViewSet, mixins.ListModelMixin, @@ -521,6 +600,7 @@ def retrieve(self, request, *args, **kwargs): def perform_create(self, serializer): serializer.save(owner=self.request.user) + class AnalysesViewSet( viewsets.GenericViewSet, mixins.CreateModelMixin, @@ -528,7 +608,6 @@ class AnalysesViewSet( mixins.DestroyModelMixin, mixins.ListModelMixin, ): - queryset = Analysis.objects.all() serializer_class = AnalysesSerializer permission_classes = [ @@ -540,12 +619,12 @@ def list(self, request, **kwargs): analyses = AnalysesSerializer(queryset, many=True).data return Response(analyses) + class AnalysisResultsViewSet( viewsets.GenericViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, ): - queryset = AnalysisResult.objects.all() serializer_class = AnalysisResultsSerializer diff --git a/backend/core/routers.py b/backend/core/routers.py index 617ce2fc..fb014092 100644 --- a/backend/core/routers.py +++ b/backend/core/routers.py @@ -13,7 +13,7 @@ ConfirmedFileViewSet, UnconfirmedFileViewSet, ConfirmFileUploadApiView, - DeleteFileUploadApiView, + DeleteFileUploadApiView, PrivateFileUploadApiView, PrivateFileViewSet, ) from core.user.viewsets import ActivateUserViewSet, UserInfoApiView from core.auth.viewsets import LoginViewSet, RegistrationViewSet, RefreshViewSet @@ -41,6 +41,7 @@ router.register( r"files/uploaded/confirmed", ConfirmedFileViewSet, basename="confirmed-files" ) +router.register(r'files/uploaded/private', PrivateFileViewSet, basename='private-files') router.register( r"files/uploaded/unconfirmed", UnconfirmedFileViewSet, basename="unconfirmed-files" ) @@ -55,6 +56,7 @@ *router.urls, path("bulk-upload/", BulkUploadApiView.as_view()), path("zip-upload/", ZipUploadApiView.as_view()), + path("private-upload/", PrivateFileUploadApiView.as_view()), re_path(r"files/uploaded/unconfirmed/confirm/(?P[\w\d]+)", ConfirmFileUploadApiView.as_view()), re_path(r"files/uploaded/unconfirmed/delete/(?P[\w\d]+)", DeleteFileUploadApiView.as_view()), path("api-auth/", include("rest_framework.urls")), diff --git a/backend/core/tests.py b/backend/core/tests.py index 720992c6..a64cb030 100644 --- a/backend/core/tests.py +++ b/backend/core/tests.py @@ -438,7 +438,7 @@ def test_add_alter_delete_single_file(self): self.assertEqual(f.description, expected_description) self.assertEqual(f.license.label, self.license_label) self.assertEqual(f.family.label, self.family_label) - self.assertEqual(f.version, '') + self.assertEqual(f.version, None) # how many tags are there ? self.assertEqual(len(f.tags.all()), 0) now = timezone.now() diff --git a/frontendVue3/src/components/FeatureModelTable.vue b/frontendVue3/src/components/FeatureModelTable.vue index d66e7b2d..afe787db 100644 --- a/frontendVue3/src/components/FeatureModelTable.vue +++ b/frontendVue3/src/components/FeatureModelTable.vue @@ -5,7 +5,7 @@ :loading="props.loading" :headers="headers" :items="filteredItems" - items-per-page="5" + items-per-page="10" :search="search" sort-by.sync="sortBy" sort-desc="sortDesc" @@ -21,6 +21,27 @@ vertical > + + + + + + See local storage + + + Upload from local storage @@ -161,7 +161,7 @@ let formData = reactive({ label: '', description: '', files: null, - license: null, + license: "CC BY - SA 4.0 DEED", family: null, version: '', tags: [], @@ -174,11 +174,6 @@ const showDetails = ref(false); let fileRules = [(v) => !!v || 'File is required']; -let { licenses } = storeToRefs(fileStore); -let licenseRules = [(v) => !!v || 'License is required']; - -let familyRules = [(v) => !!v || 'Family is required']; - let checkboxRules = [(v) => !!v || 'Checkbox must be checked']; diff --git a/frontendVue3/src/components/upload_cards/file_create/PrivateUpload.vue b/frontendVue3/src/components/upload_cards/file_create/PrivateUpload.vue new file mode 100644 index 00000000..fb11d98e --- /dev/null +++ b/frontendVue3/src/components/upload_cards/file_create/PrivateUpload.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontendVue3/src/components/upload_cards/file_create/Single.vue b/frontendVue3/src/components/upload_cards/file_create/Single.vue index ae716572..dd000493 100644 --- a/frontendVue3/src/components/upload_cards/file_create/Single.vue +++ b/frontendVue3/src/components/upload_cards/file_create/Single.vue @@ -2,6 +2,20 @@ + + + - - - - - - - + + + + + @@ -67,7 +68,6 @@ item-title="label" item-value="id" :required="true" - :rules="familyRules" variant="outlined" density="comfortable" hint="Add to or create new family" @@ -176,7 +176,7 @@ + + diff --git a/frontendVue3/src/components/upload_cards/file_create/Zip.vue b/frontendVue3/src/components/upload_cards/file_create/Zip.vue index 22c56dab..9cce6ed5 100644 --- a/frontendVue3/src/components/upload_cards/file_create/Zip.vue +++ b/frontendVue3/src/components/upload_cards/file_create/Zip.vue @@ -2,6 +2,20 @@ + + + + - - - - - - + + + + @@ -126,7 +127,7 @@ !!v || 'Label is required']; let descriptionRules = [ - (v) => !!v || 'Description is required', (v) => v.length <= 250 || 'Max 250 characters please', ]; let fileRules = [(v) => !!v || 'File is required']; -let { licenses, tags, myOwnTags } = storeToRefs(fileStore); -let licenseRules = [(v) => !!v || 'License is required']; +let { tags, myOwnTags } = storeToRefs(fileStore); -let familyRules = [(v) => !!v || 'Family is required']; let checkboxRules = [(v) => !!v || 'Checkbox must be checked']; -//let versionRules = [(v) => !!v || 'Version is required']; - let addTagMenu = ref(false); let addFamilyMenu = ref(false); +const redirectToLicensePage = () => { + window.open('https://creativecommons.org/licenses/by-sa/4.0/deed.de', '_blank'); +}; diff --git a/frontendVue3/src/store/file.js b/frontendVue3/src/store/file.js index d75097ce..12fec347 100644 --- a/frontendVue3/src/store/file.js +++ b/frontendVue3/src/store/file.js @@ -14,6 +14,8 @@ export const useFileStore = defineStore('file', { confirmedFeatureModels: [], myConfirmedFeatureModels: [], featureModels: [], + defaultLicense: null, + myPrivateFeatureModels:[] }), getters: { myOwnTags(state) { @@ -29,6 +31,11 @@ export const useFileStore = defineStore('file', { this.confirmedFeatureModels = response.data; }); }, + fetchMyPrivateFeatureModels() { + api.get(`${API_URL}files/uploaded/private/`).then((response) => { + this.myPrivateFeatureModels = response.data; + }); + }, async fetchMyConfirmedFeatureModels() { const id = useAuthStore().currentUser.id; api.get(`${API_URL}files/uploaded/confirmed/?owner=` + id).then( @@ -77,6 +84,7 @@ export const useFileStore = defineStore('file', { }, fetchLicenses() { api.get(`${API_URL}licenses/`).then((response) => { + this.defaultLicense = response.data.filter((li) => li.label === "CC BY - SA 4.0 DEED").shift(); this.licenses = response.data; }); }, @@ -140,6 +148,31 @@ export const useFileStore = defineStore('file', { return false; }) }, + async uploadPrivateFile(data) { + const appStore = useAppStore(); + return await api + .post(`${API_URL}private-upload/`, data, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + .then(() => { + appStore.updateSnackbar( + 'Upload successfully!', + 'success', + 5000, + true + ); + return true + }) + .catch((error) => { + appStore.updateSnackbar( + 'Error! ' + error.message, + 'error', + 5000, + true + ); + return false; + }); + }, async uploadTag(payload) { // payload = { label, description, is_public } const appStore = useAppStore(); diff --git a/frontendVue3/src/views/HistoryDetail.vue b/frontendVue3/src/views/HistoryDetail.vue index 2ee73f46..1805e2b4 100644 --- a/frontendVue3/src/views/HistoryDetail.vue +++ b/frontendVue3/src/views/HistoryDetail.vue @@ -221,7 +221,7 @@ :no-data-text="`No feature models in ${family.label} yet`" :addable="false" headline="Feature Models of Family" - /> + :available-tags="tags"/> @@ -235,16 +235,15 @@ import {busyBoxConfigs} from "@/assets/busyBoxAnalyzeExample"; import {useDisplay, useTheme} from "vuetify"; import {VSkeletonLoader} from 'vuetify/labs/VSkeletonLoader' import LineChart from '@/components/Charts/LineChart.vue'; - - - - - +import {useFileStore} from "@/store/file"; +import {storeToRefs} from "pinia"; const breakpoints = useDisplay(); const theme = useTheme(); const route = useRoute(); const API_URL = import.meta.env.VITE_APP_DOMAIN; +const fileStore = useFileStore(); +const { tags } = storeToRefs(useFileStore()); const family = ref({}); const files = ref([]); @@ -364,5 +363,6 @@ function onElementHover(elem) { onMounted(async () => { await getFamily(); await fetchFeatureModelOfFamily(); + fileStore.fetchTags(); }); diff --git a/frontendVue3/src/views/Home.vue b/frontendVue3/src/views/Home.vue index 470e38dc..ddf09111 100644 --- a/frontendVue3/src/views/Home.vue +++ b/frontendVue3/src/views/Home.vue @@ -8,6 +8,7 @@ @@ -37,7 +38,7 @@ import { onMounted, ref } from 'vue'; import { storeToRefs } from 'pinia'; import { useFileStore } from '@/store/file'; -const { confirmedFeatureModels } = storeToRefs(useFileStore()); +const { confirmedFeatureModels, tags } = storeToRefs(useFileStore()); const fileStore = useFileStore(); const search = ''; @@ -79,7 +80,6 @@ const defaultItem = { }; const licenses = []; const families = []; -const tags = []; const check1 = false; const check2 = false; const check3 = false; @@ -127,5 +127,6 @@ const tutorialSteps = [ onMounted(() => { fileStore.fetchConfirmedFeatureModels(); + fileStore.fetchTags(); }); diff --git a/frontendVue3/src/views/Models.vue b/frontendVue3/src/views/Models.vue index 38ff0867..e41be55b 100644 --- a/frontendVue3/src/views/Models.vue +++ b/frontendVue3/src/views/Models.vue @@ -5,8 +5,10 @@