From 1505dd9c14d9d43fdac9728f34e907dcaecba1cc Mon Sep 17 00:00:00 2001 From: Souad El Mansouri Date: Sat, 4 Feb 2023 23:42:40 +0100 Subject: [PATCH] Create new feature to enable creation of new activities (#539) * Create activity reducer with initial state for all activities * Create activity actions file with setActivities function * Update configureStore to include activityReducer * Created a generic activities objects to use meanwhile creating backend for activities * Create Activities component and add button in header to render it * Connect activities component to activities store * Create action types constants * Remove unused files * Create activities video carousel * Create activities video carousel * Create activity card * Add projects count icon and activity tags to the card * Add creator to activity card * Create creator component to handle multipe activity creator case and display them in activity card * Show full creator image on hover * Ajust activity cards and handle display of more than 3 tags * Create activity creation form container * Create form steps components and form progressBar * Add translation for BROWS ACTIVITIES Button * Create reusable dynamic progress bar * Create activity creation step1 form * Create create activity step 2 form inputs * Create separate component for react quill text fields * Create AddMore and MaterialsUsed component * Add handleOnChange an on blur for materials used inputs * Create formLabel and UploadFile components * Add validation to step1 fields * Create step3 input blocks * Add form steps verification * Add defaultValue to input and inputText to display values entered when checking back previous steps * Add handleImageChange to handle all fileUpload instances and fix form responsiveness issues * Add validation to fields accepting images and Create upload image functionality * Add at least one material required validation for materialsUsed array field * Fix inspiringExemplesImages append files issue * Try to fix appending multiple inspiringExemplesImages images to newActivity state by adding keys to uploadFiles component instances and add promise to set state * Create separate states for step verifiedStep and newActivity and use name instead of label in IputText and send only required props as parameter * Update InputText and Input components and according script functions to accept only needed props * Update materialsUsed and all other fields using Input and InputText components and make them update form state correctly * Update UploadFiles and uploadFile scripts to accept the new states and handle multiple inputs feeding same field in the state issue * Upload images to local and setNewActivityObject with urls * Handle video change on uploadFile * Fix inspiringExemplesImages files_to_upload issue to upload correctly * updateState with formik file values on form submit * Add hided submitButton with ref and click on setState from refactorNewActivityObject to upload files after state is updated * Create activities app backend * Create activities app * Add materials_udes_image field to activity model * Add category and add order to making steps * Add inspiring artists * started updating handleFileFieldChange * Add inspired_from_activity field to Project model and run migrations * Create UploadMedia and FileField classes to handle file input changes * Add inputLabel to display label in inputs not having FormLabel and Add artist full name input to artist section * Move upload media to uploadFileScript and Change it to return promise and create appendFile to field for file field accepting multiple files * Create activity serializers and ListAPIView * Create activity serializers create method and combine fields data for nested objects in createActivityScripts * Add creator to created activity * Fixing input value undefined * Fix styled-components package import issue * Refactor activity object returned from backend to fit form fields for edit and Add redirection and routes for activity edit * Fix Input and InputText display value from state issue * Create map of field names and activity object attribute to help deserialize and display data in the form for edit * Successfully Update activity and all related fields * Fix delete activity issue and add loading to activities veiw * Remove commented code from activities view * Fix refresh activities list after delete and update issue by using useSelector hook instead of reading reducer state from props * Fix Display InputText errors issue * Remove prev selected files if new files have errors and display error * Add preview image feature to UploadFile component * Add preview image to UploadFile component * Preview image delete works, add validation for activity images on update when deleting all urls * Fix activity without unrequired fields creation issue by removing field keys from api object instead of sending empty objects * Changed all form state to be handled by formik including array of objects fields and updated create activity * Update step verification to verify step when required field provided with no error and let user see msgs * Add function to fill form from activity object and call update activity * User stats enhancement (#505) * Use same border radius for next and prev buttons * refactor button border radius code * reverted master * user's profile header refactor * Revert "User stats enhancement (#505)" (#527) This reverts commit 4aa7cc1f7120106977c5527639ac091dccb3c7ab. * User stats redesign (#526) * Use same border radius for next and prev buttons * refactor button border radius code * reverted master * user stats restructure * patch to handle longusernames Co-authored-by: Deepanshu039 * update and create working with no issues after refactor * Activitylog feature (#523) * Use same border radius for next and prev buttons * refactor button border radius code * reverted master * activitylog feature to keep the track of user's activity * patch after #1st review * migration files of activity-log * react-icons package used in activity-log * package-lock file with minimal changes Co-authored-by: Deepanshu039 * add --max_old_space_size=4096 to react-script options in package.json (#529) Co-authored-by: Ndibe Raymond Olisaemeka * add GENERATE_SOURCEMAP=false to .env file to solve (#530) the heap out of memory problem Co-authored-by: Ndibe Raymond Olisaemeka * Revert "add --max_old_space_size=4096 to react-script options in package.json (#529)" (#531) This reverts commit 9444c3fe149eb7a3d0e59644321792bd54d68c22. * Use formik field Array to display making steps and inspiring examples and fix all update issues now creation and update working correctly * Badge titles on user's profile (#524) * Use same border radius for next and prev buttons * refactor button border radius code * reverted master * automated and custom badges on user's profile * initially populate badges table * #1 patch after review * feature: change original badge title from admin panel * migration file of badge table Co-authored-by: Deepanshu039 * Add empty object validation and file compression * Handle video validation Upload and preview by UploadFile component * Add save activity functionality and first section of activity details view * Add verification on clicking next button * Fix video url preview and empty url issues * Add regex to validate video urls * Use iframe for video url previews and to read videos in activity details view * Add pdf creation component * Add generate pdf of activity and style activity detail view * commit pdfStyle and scripts * merged remote master * Add build project based on activity functionality * Add breadCrumb nav * Fix styling issues * Redirect user to activity_details after creation or edit * handle error comming from server and add permission restrictions * restyle pdf * Restrict activity creation to educators staff and moderators * Display inspired_projects count in activity cards * Add toggle publish functionality and fix activity details style issues * Fix making steps empty object validation * Add hindi missing transaltion to activity form * Restyle linked projects count in activity card * Fix activity details quill editor styling * Add footer to pdf * Add visited links to breadCrumb * Change breadCrumb separators * Fix validation max values and updated translation properties accordingly * Display only published activities to non staff or moderator users * Add condition before displaying store variables to fix oppening on firefox issue * Update breadCrumb to handle all links creators projects and activities * Add Create Activity link and Update href to react router Links in humbergerMenu * Add my activities and unpublished activities links to profile menu * Remove unbreakable from making steps block on pdf and put activity_id in params instead of url in project create * Fix empty height space generated by react quill on firefox * Fix images preview stretch issue * Add upload progress * Fix breadCrumbs default redirection to projects issue * Increase activity form title weight * Capitalize breadCumbs Links * Decrease breadCrumbs Links font weigth * Change activity cards LinkedProjects pill text color and make count number bolder * Create api endpoints and dispatchers to fetch published unPublished and userActivities * Add making steps order and list dots to materials required * Add link to activity from pdf * Remove prints and console logs * Capitalize input labels of activity creation form * Make breadCrumbs navBar stick to top when scrolling down * Change activity text and titles color to black to follow projects style * Add link to activity creator profile from created on date in activity details view * Render error page if activity list is empty * Move breadCrumbs navbar into header to have fixed position * Update breadCrumbs navBar to properly display routes * Update activities view to fetch data from api and display loading meanwhile * merge remote master * Add changes * update pdfmake * Add linked_projects component * Add activity linked projects view and link * Call upload file to DO * updated upload to DO and upload video to cloudinary but still need review and test from Raymond --------- Co-authored-by: Deepanshu Gautam <92849065+Deepanshu039@users.noreply.github.com> Co-authored-by: Suchakra Sharma Co-authored-by: Deepanshu039 Co-authored-by: Ndibe Raymond Olisaemeka Co-authored-by: Ndibe Raymond Olisaemeka --- .../compose/celery/requirements.txt | 2 +- .../compose/flower/requirements.txt | 2 +- zubhub_backend/compose/web/dev/start | 1 + zubhub_backend/compose/web/prod/start | 1 + zubhub_backend/compose/web/requirements.txt | 2 +- zubhub_backend/zubhub/APIS/urls.py | 7 +- zubhub_backend/zubhub/activities/__init__.py | 0 zubhub_backend/zubhub/activities/admin.py | 3 + zubhub_backend/zubhub/activities/apps.py | 5 + .../activities/migrations/0001_initial.py | 103 + .../migrations/0002_auto_20220814_0738.py | 44 + .../migrations/0003_auto_20220814_1703.py | 25 + ...ame_order_activitymakingstep_step_order.py | 18 + .../0005_rename_image_url_image_file_url.py | 18 + .../migrations/0006_activity_slug.py | 19 + .../migrations/0007_auto_20220819_1640.py | 24 + .../migrations/0008_auto_20220829_1530.py | 23 + .../zubhub/activities/migrations/__init__.py | 0 zubhub_backend/zubhub/activities/models.py | 138 + .../zubhub/activities/permissions.py | 22 + .../zubhub/activities/serializers.py | 186 + zubhub_backend/zubhub/activities/tests.py | 3 + zubhub_backend/zubhub/activities/urls.py | 16 + zubhub_backend/zubhub/activities/utils.py | 60 + zubhub_backend/zubhub/activities/views.py | 142 + zubhub_backend/zubhub/activitylog/__init__.py | 1 + zubhub_backend/zubhub/activitylog/admin.py | 6 + zubhub_backend/zubhub/activitylog/apps.py | 5 + .../activitylog/migrations/0001_initial.py | 31 + .../zubhub/activitylog/migrations/__init__.py | 0 zubhub_backend/zubhub/activitylog/models.py | 27 + .../zubhub/activitylog/pagination.py | 6 + .../zubhub/activitylog/permissions.py | 12 + .../zubhub/activitylog/serializers.py | 30 + zubhub_backend/zubhub/activitylog/tests.py | 3 + zubhub_backend/zubhub/activitylog/urls.py | 10 + zubhub_backend/zubhub/activitylog/utils.py | 22 + zubhub_backend/zubhub/activitylog/views.py | 28 + zubhub_backend/zubhub/creators/admin.py | 30 +- .../management/commands/initial_badges.txt | 14 + .../commands/populate_initial_badges.py | 22 + .../migrations/0009_alter_setting_contact.py | 18 + .../migrations/0009_auto_20220822_0842.py | 32 + .../migrations/0010_merge_20220921_2226.py | 14 + zubhub_backend/zubhub/creators/models.py | 28 +- zubhub_backend/zubhub/creators/serializers.py | 17 +- zubhub_backend/zubhub/creators/utils.py | 66 +- zubhub_backend/zubhub/creators/views.py | 10 +- .../migrations/0007_tag_tag_name_gin_idx.py | 18 + .../0008_project_inspired_from_activity.py | 20 + zubhub_backend/zubhub/projects/models.py | 9 +- zubhub_backend/zubhub/projects/serializers.py | 64 +- zubhub_backend/zubhub/projects/views.py | 49 +- .../templates/activitylog/bookmark.html | 1 + .../zubhub/templates/activitylog/clap.html | 1 + .../zubhub/templates/activitylog/comment.html | 1 + .../zubhub/templates/activitylog/follow.html | 1 + zubhub_backend/zubhub/zubhub/admin.py | 25 +- .../zubhub/migrations/0007_ambassadors.py | 25 + .../migrations/0008_ambassadors_projects.py | 19 + .../zubhub/migrations/0009_challenge.py | 25 + zubhub_backend/zubhub/zubhub/models.py | 33 + zubhub_backend/zubhub/zubhub/serializers.py | 53 +- zubhub_backend/zubhub/zubhub/settings.py | 7 +- zubhub_backend/zubhub/zubhub/urls.py | 17 +- zubhub_backend/zubhub/zubhub/views.py | 29 +- zubhub_frontend/zubhub/.env.example | 3 +- zubhub_frontend/zubhub/package-lock.json | 13425 ++++++++++------ zubhub_frontend/zubhub/package.json | 7 +- .../zubhub/public/locales/en/translation.json | 251 +- .../zubhub/public/locales/hi/translation.json | 242 +- zubhub_frontend/zubhub/public/logo.png | Bin 0 -> 17664 bytes zubhub_frontend/zubhub/src/App.js | 115 +- zubhub_frontend/zubhub/src/api/api.js | 136 +- .../src/assets/images/logos/logo-google.png | Bin 0 -> 7728 bytes .../zubhub/src/assets/images/new_stuff.svg | 3 + .../src/assets/js/icons/projectsCountIcon.js | 15 + .../actionIconsContainerStyles.js | 99 + .../components/activity/activityStyle.js | 100 + .../components/breadCrumb/breadCrumbStyle.js | 73 + .../styles/components/button/buttonStyles.js | 26 +- .../styles/components/creator/creatorStyle.js | 25 + .../generatePdf/generatePdfStyle.js | 97 + .../user_activitylog/userActivitylogStyles.js | 47 + .../zubhub/src/assets/js/styles/index.js | 85 + .../views/activities/activitiesStyles.js | 145 + .../activity_details/activityDetailsStyles.js | 235 + .../views/ambassadors/ambassadorsStyles.js | 47 + .../styles/views/challenge/challengeStyles.js | 26 + .../create_activity/createActivityStyles.js | 71 + .../create_project/createProjectStyles.js | 7 + .../views/page_wrapper/pageWrapperStyles.js | 17 +- .../js/styles/views/profile/profileStyles.js | 140 +- .../zubhub/src/assets/js/utils/scripts.js | 91 +- .../actionIconsContainer.jsx | 117 + .../src/components/activity/activity.jsx | 222 + .../components/activity/activityScripts.js | 54 + .../zubhub/src/components/addMore/addMore.jsx | 24 + .../src/components/breadCrumb/breadCrumb.jsx | 160 + .../zubhub/src/components/button/Button.js | 15 +- .../zubhub/src/components/creator/creator.jsx | 26 + .../src/components/form_labels/formLabel.jsx | 25 + .../components/generatePdf/generatePdf.jsx | 201 + .../generatePdf/generatePdfScripts.js | 205 + .../hamburger_menu/HamburgerMenu.jsx | 122 +- .../zubhub/src/components/input/input.jsx | 96 + .../src/components/input/inputScripts.js | 61 + .../src/components/inputText/inputText.jsx | 116 + .../components/inputText/inputTextScripts.js | 27 + .../materialsUsed/materialsUsed.jsx | 129 + .../multiStepProgressBar.jsx | 49 + .../multiStepProgressBarScripts.js | 19 + .../src/components/upload_file/uploadFile.jsx | 183 + .../upload_file/uploadFileScripts.js | 582 + .../user_activitylog/UserActivitylog.jsx | 86 + .../zubhub/src/store/actionTypes.js | 2 + .../src/store/actions/activityActions.js | 182 + .../zubhub/src/store/actions/userActions.js | 66 + .../zubhub/src/store/configureStore.js | 2 +- .../src/store/reducers/activityReducer.js | 39 + .../zubhub/src/store/reducers/index.js | 2 + .../zubhub/src/views/PageWrapper.jsx | 125 +- .../src/views/activities/activities.jsx | 121 + .../activityDetailsScripts.js | 42 + .../activity_details/activity_details.jsx | 663 + .../src/views/ambassadors/Ambassadors.jsx | 166 + .../views/ambassadors/ambassadorsScripts.js | 48 + .../zubhub/src/views/challenge/Challenge.jsx | 83 + .../create_activity/createActivityScripts.js | 599 + .../views/create_activity/create_activity.jsx | 325 + .../create_activity/create_activity_step1.jsx | 101 + .../create_activity/create_activity_step2.jsx | 185 + .../create_activity/create_activity_step3.jsx | 448 + .../views/create_project/CreateProject.jsx | 7 +- .../create_project/createProjectScripts.js | 9 +- .../src/views/edit_profile/EditProfile.jsx | 111 +- .../views/edit_profile/editProfileScripts.js | 30 + .../views/linked_projects/LinkedProjects.jsx | 84 + .../linked_projects/LinkedProjectsScripts.js | 25 + .../zubhub/src/views/profile/Profile.jsx | 338 +- .../zubhub/src/views/projects/Projects.jsx | 17 +- 141 files changed, 18462 insertions(+), 5098 deletions(-) create mode 100644 zubhub_backend/zubhub/activities/__init__.py create mode 100644 zubhub_backend/zubhub/activities/admin.py create mode 100644 zubhub_backend/zubhub/activities/apps.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0001_initial.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0002_auto_20220814_0738.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0003_auto_20220814_1703.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0004_rename_order_activitymakingstep_step_order.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0005_rename_image_url_image_file_url.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0006_activity_slug.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0007_auto_20220819_1640.py create mode 100644 zubhub_backend/zubhub/activities/migrations/0008_auto_20220829_1530.py create mode 100644 zubhub_backend/zubhub/activities/migrations/__init__.py create mode 100644 zubhub_backend/zubhub/activities/models.py create mode 100644 zubhub_backend/zubhub/activities/permissions.py create mode 100644 zubhub_backend/zubhub/activities/serializers.py create mode 100644 zubhub_backend/zubhub/activities/tests.py create mode 100644 zubhub_backend/zubhub/activities/urls.py create mode 100644 zubhub_backend/zubhub/activities/utils.py create mode 100644 zubhub_backend/zubhub/activities/views.py create mode 100644 zubhub_backend/zubhub/activitylog/__init__.py create mode 100644 zubhub_backend/zubhub/activitylog/admin.py create mode 100644 zubhub_backend/zubhub/activitylog/apps.py create mode 100644 zubhub_backend/zubhub/activitylog/migrations/0001_initial.py create mode 100644 zubhub_backend/zubhub/activitylog/migrations/__init__.py create mode 100644 zubhub_backend/zubhub/activitylog/models.py create mode 100644 zubhub_backend/zubhub/activitylog/pagination.py create mode 100644 zubhub_backend/zubhub/activitylog/permissions.py create mode 100644 zubhub_backend/zubhub/activitylog/serializers.py create mode 100644 zubhub_backend/zubhub/activitylog/tests.py create mode 100644 zubhub_backend/zubhub/activitylog/urls.py create mode 100644 zubhub_backend/zubhub/activitylog/utils.py create mode 100644 zubhub_backend/zubhub/activitylog/views.py create mode 100644 zubhub_backend/zubhub/creators/management/commands/initial_badges.txt create mode 100644 zubhub_backend/zubhub/creators/management/commands/populate_initial_badges.py create mode 100644 zubhub_backend/zubhub/creators/migrations/0009_alter_setting_contact.py create mode 100644 zubhub_backend/zubhub/creators/migrations/0009_auto_20220822_0842.py create mode 100644 zubhub_backend/zubhub/creators/migrations/0010_merge_20220921_2226.py create mode 100644 zubhub_backend/zubhub/projects/migrations/0007_tag_tag_name_gin_idx.py create mode 100644 zubhub_backend/zubhub/projects/migrations/0008_project_inspired_from_activity.py create mode 100644 zubhub_backend/zubhub/templates/activitylog/bookmark.html create mode 100644 zubhub_backend/zubhub/templates/activitylog/clap.html create mode 100644 zubhub_backend/zubhub/templates/activitylog/comment.html create mode 100644 zubhub_backend/zubhub/templates/activitylog/follow.html create mode 100644 zubhub_backend/zubhub/zubhub/migrations/0007_ambassadors.py create mode 100644 zubhub_backend/zubhub/zubhub/migrations/0008_ambassadors_projects.py create mode 100644 zubhub_backend/zubhub/zubhub/migrations/0009_challenge.py create mode 100644 zubhub_frontend/zubhub/public/logo.png create mode 100644 zubhub_frontend/zubhub/src/assets/images/logos/logo-google.png create mode 100644 zubhub_frontend/zubhub/src/assets/images/new_stuff.svg create mode 100644 zubhub_frontend/zubhub/src/assets/js/icons/projectsCountIcon.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/actionIconsContainer/actionIconsContainerStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/activity/activityStyle.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/breadCrumb/breadCrumbStyle.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/creator/creatorStyle.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/generatePdf/generatePdfStyle.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/components/user_activitylog/userActivitylogStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/activities/activitiesStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/activity_details/activityDetailsStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/ambassadors/ambassadorsStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/challenge/challengeStyles.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/styles/views/create_activity/createActivityStyles.js create mode 100644 zubhub_frontend/zubhub/src/components/actionIconsContainer/actionIconsContainer.jsx create mode 100644 zubhub_frontend/zubhub/src/components/activity/activity.jsx create mode 100644 zubhub_frontend/zubhub/src/components/activity/activityScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/addMore/addMore.jsx create mode 100644 zubhub_frontend/zubhub/src/components/breadCrumb/breadCrumb.jsx create mode 100644 zubhub_frontend/zubhub/src/components/creator/creator.jsx create mode 100644 zubhub_frontend/zubhub/src/components/form_labels/formLabel.jsx create mode 100644 zubhub_frontend/zubhub/src/components/generatePdf/generatePdf.jsx create mode 100644 zubhub_frontend/zubhub/src/components/generatePdf/generatePdfScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/input/input.jsx create mode 100644 zubhub_frontend/zubhub/src/components/input/inputScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/inputText/inputText.jsx create mode 100644 zubhub_frontend/zubhub/src/components/inputText/inputTextScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/materialsUsed/materialsUsed.jsx create mode 100644 zubhub_frontend/zubhub/src/components/multi_step_progress_bar/multiStepProgressBar.jsx create mode 100644 zubhub_frontend/zubhub/src/components/multi_step_progress_bar/multiStepProgressBarScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/upload_file/uploadFile.jsx create mode 100644 zubhub_frontend/zubhub/src/components/upload_file/uploadFileScripts.js create mode 100644 zubhub_frontend/zubhub/src/components/user_activitylog/UserActivitylog.jsx create mode 100644 zubhub_frontend/zubhub/src/store/actionTypes.js create mode 100644 zubhub_frontend/zubhub/src/store/actions/activityActions.js create mode 100644 zubhub_frontend/zubhub/src/store/reducers/activityReducer.js create mode 100644 zubhub_frontend/zubhub/src/views/activities/activities.jsx create mode 100644 zubhub_frontend/zubhub/src/views/activity_details/activityDetailsScripts.js create mode 100644 zubhub_frontend/zubhub/src/views/activity_details/activity_details.jsx create mode 100644 zubhub_frontend/zubhub/src/views/ambassadors/Ambassadors.jsx create mode 100644 zubhub_frontend/zubhub/src/views/ambassadors/ambassadorsScripts.js create mode 100644 zubhub_frontend/zubhub/src/views/challenge/Challenge.jsx create mode 100644 zubhub_frontend/zubhub/src/views/create_activity/createActivityScripts.js create mode 100644 zubhub_frontend/zubhub/src/views/create_activity/create_activity.jsx create mode 100644 zubhub_frontend/zubhub/src/views/create_activity/create_activity_step1.jsx create mode 100644 zubhub_frontend/zubhub/src/views/create_activity/create_activity_step2.jsx create mode 100644 zubhub_frontend/zubhub/src/views/create_activity/create_activity_step3.jsx create mode 100644 zubhub_frontend/zubhub/src/views/linked_projects/LinkedProjects.jsx create mode 100644 zubhub_frontend/zubhub/src/views/linked_projects/LinkedProjectsScripts.js diff --git a/zubhub_backend/compose/celery/requirements.txt b/zubhub_backend/compose/celery/requirements.txt index 4af6f4977..416b7efca 100644 --- a/zubhub_backend/compose/celery/requirements.txt +++ b/zubhub_backend/compose/celery/requirements.txt @@ -26,7 +26,7 @@ django-js-asset>=1.2.2 django-mptt>=0.11.0 django-rest-auth>=0.9.5 django-rest-swagger>=2.2.0 -django-summernote>=0.8.11.6 +django-summernote==0.8.11.6 django-treebeard>=4.5.1 djangorestframework>=3.11.2 factory-boy>=2.12.0 diff --git a/zubhub_backend/compose/flower/requirements.txt b/zubhub_backend/compose/flower/requirements.txt index 19b0c2455..9fffa608d 100644 --- a/zubhub_backend/compose/flower/requirements.txt +++ b/zubhub_backend/compose/flower/requirements.txt @@ -25,7 +25,7 @@ django-js-asset>=1.2.2 django-mptt>=0.11.0 django-rest-auth>=0.9.5 django-rest-swagger>=2.2.0 -django-summernote>=0.8.11.6 +django-summernote==0.8.11.6 django-treebeard>=4.5.1 djangorestframework>=3.11.2 factory-boy>=2.12.0 diff --git a/zubhub_backend/compose/web/dev/start b/zubhub_backend/compose/web/dev/start index cde5bcd3d..eefac2be7 100644 --- a/zubhub_backend/compose/web/dev/start +++ b/zubhub_backend/compose/web/dev/start @@ -19,6 +19,7 @@ python /zubhub_backend/zubhub/manage.py createcachetable python /zubhub_backend/zubhub/manage.py populate_countries python /zubhub_backend/zubhub/manage.py populate_categories python /zubhub_backend/zubhub/manage.py populate_initial_creator_tags +python /zubhub_backend/zubhub/manage.py populate_initial_badges exec /usr/local/bin/gunicorn zubhub.wsgi --reload --threads=3 --timeout 155 --bind 0.0.0.0:8000 \ --access-logfile - --error-logfile - --chdir /zubhub_backend/zubhub diff --git a/zubhub_backend/compose/web/prod/start b/zubhub_backend/compose/web/prod/start index 982c6e615..89995e113 100644 --- a/zubhub_backend/compose/web/prod/start +++ b/zubhub_backend/compose/web/prod/start @@ -15,6 +15,7 @@ python /zubhub_backend/zubhub/manage.py createcachetable python /zubhub_backend/zubhub/manage.py populate_countries python /zubhub_backend/zubhub/manage.py populate_initial_creator_tags python /zubhub_backend/zubhub/manage.py populate_categories +python /zubhub_backend/zubhub/manage.py populate_initial_badges exec /usr/local/bin/gunicorn zubhub.wsgi --threads=3 --timeout 155 --bind 0.0.0.0:8000 \ --access-logfile - --error-logfile - --chdir /zubhub_backend/zubhub diff --git a/zubhub_backend/compose/web/requirements.txt b/zubhub_backend/compose/web/requirements.txt index 8895b83e2..f2d7cbe81 100644 --- a/zubhub_backend/compose/web/requirements.txt +++ b/zubhub_backend/compose/web/requirements.txt @@ -26,7 +26,7 @@ django-js-asset>=1.2.2 django-mptt>=0.11.0 django-rest-auth>=0.9.5 django-rest-swagger>=2.2.0 -django-summernote>=0.8.11.6 +django-summernote==0.8.11.6 django-treebeard>=4.5.1 djangorestframework>=3.11.2 factory-boy>=2.12.0 diff --git a/zubhub_backend/zubhub/APIS/urls.py b/zubhub_backend/zubhub/APIS/urls.py index ecfa95c5c..4491415ae 100644 --- a/zubhub_backend/zubhub/APIS/urls.py +++ b/zubhub_backend/zubhub/APIS/urls.py @@ -1,6 +1,7 @@ from django.urls import path, include from zubhub.views import (UploadFileAPIView, DeleteFileAPIView, - HeroAPIView, HelpAPIView, PrivacyAPIView, FAQAPIView, SigGenAPIView, UploadFileToLocalAPIView) + HeroAPIView, HelpAPIView, ChallengeAPIView, PrivacyAPIView, FAQAPIView, AmbassadorsAPIView, + SigGenAPIView, UploadFileToLocalAPIView) urlpatterns = [ @@ -8,6 +9,8 @@ path('rest-auth/registration/', include('rest_auth.registration.urls')), path('creators/', include('creators.urls', namespace="creators")), path('projects/', include('projects.urls', namespace="projects")), + path('activitylog/', include('activitylog.urls', namespace="activitylog")), + path('activities/', include('activities.urls', namespace="activities")), path('notifications/', include('notifications.urls', namespace="notifications")), path('upload-file/', UploadFileAPIView, name="upload_file"), path('delete-file/', DeleteFileAPIView, name="delete_file"), @@ -17,6 +20,8 @@ path('help/', HelpAPIView.as_view(), name="help"), path('privacy/', PrivacyAPIView.as_view(), name="privacy"), path('faqs/', FAQAPIView.as_view(), name="faqs"), + path('ambassadors/', AmbassadorsAPIView.as_view(), name="ambassadors"), + path('challenge/', ChallengeAPIView.as_view(), name="challenge"), path('signature/', SigGenAPIView, name="signature_generator_api") ] diff --git a/zubhub_backend/zubhub/activities/__init__.py b/zubhub_backend/zubhub/activities/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zubhub_backend/zubhub/activities/admin.py b/zubhub_backend/zubhub/activities/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/zubhub_backend/zubhub/activities/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/zubhub_backend/zubhub/activities/apps.py b/zubhub_backend/zubhub/activities/apps.py new file mode 100644 index 000000000..39325dd9e --- /dev/null +++ b/zubhub_backend/zubhub/activities/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ActivitiesConfig(AppConfig): + name = 'activities' diff --git a/zubhub_backend/zubhub/activities/migrations/0001_initial.py b/zubhub_backend/zubhub/activities/migrations/0001_initial.py new file mode 100644 index 000000000..5fec452dc --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0001_initial.py @@ -0,0 +1,103 @@ +# Generated by Django 3.2 on 2022-08-11 11:17 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('projects', '0007_tag_tag_name_gin_idx'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('title', models.CharField(max_length=1000)), + ('learning_goals', models.TextField(blank=True, max_length=10000)), + ('facilitation_tips', models.TextField(blank=True, max_length=10000)), + ('motivation', models.TextField(blank=True, max_length=10000)), + ('video', models.URLField(blank=True, max_length=1000, null=True)), + ('materials_used', models.CharField(max_length=5000)), + ('views_count', models.IntegerField(blank=True, default=0)), + ('saved_count', models.IntegerField(blank=True, default=0)), + ('created_on', models.DateTimeField(default=django.utils.timezone.now)), + ('publish', models.BooleanField(default=False)), + ('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activities', to='projects.category')), + ('creators', models.ManyToManyField(related_name='activities_created', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Image', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image_url', models.URLField(max_length=1000)), + ('public_id', models.CharField(blank=True, max_length=1000)), + ], + ), + migrations.CreateModel( + name='InspiringExamples', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, max_length=10000)), + ('credit', models.CharField(blank=True, max_length=1000)), + ('activity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inspiring_examples', to='activities.activity')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='activities.image')), + ], + ), + migrations.CreateModel( + name='InspiringArtist', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('short_biography', models.TextField(blank=True, max_length=10000)), + ('name', models.CharField(max_length=100)), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='activities.image')), + ], + ), + migrations.CreateModel( + name='ActivityMakingSteps', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('description', models.TextField(blank=True, max_length=10000)), + ('order', models.IntegerField()), + ('activity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='making_steps', to='activities.activity')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='activities.image')), + ], + ), + migrations.CreateModel( + name='ActivityImages', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('activity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activity_images', to='activities.activity')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='activities.image')), + ], + ), + migrations.AddField( + model_name='activity', + name='inspiring_artist', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inspiring_artist_activities', to='activities.inspiringartist'), + ), + migrations.AddField( + model_name='activity', + name='materials_used_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='activities.image'), + ), + migrations.AddField( + model_name='activity', + name='saved_by', + field=models.ManyToManyField(blank=True, related_name='activities_saved', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='activity', + name='views', + field=models.ManyToManyField(blank=True, related_name='activities_viewed', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0002_auto_20220814_0738.py b/zubhub_backend/zubhub/activities/migrations/0002_auto_20220814_0738.py new file mode 100644 index 000000000..f5ac6930a --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0002_auto_20220814_0738.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2 on 2022-08-14 07:38 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='created_on', + field=models.DateTimeField(default=django.utils.timezone.now, null=True), + ), + migrations.AlterField( + model_name='activity', + name='materials_used', + field=models.TextField(max_length=5000), + ), + migrations.AlterField( + model_name='activity', + name='publish', + field=models.BooleanField(default=False, null=True), + ), + migrations.AlterField( + model_name='activity', + name='title', + field=models.CharField(max_length=500), + ), + migrations.AlterField( + model_name='image', + name='public_id', + field=models.TextField(blank=True, max_length=1000), + ), + migrations.AlterField( + model_name='inspiringexamples', + name='credit', + field=models.TextField(blank=True, max_length=1000), + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0003_auto_20220814_1703.py b/zubhub_backend/zubhub/activities/migrations/0003_auto_20220814_1703.py new file mode 100644 index 000000000..b2a2be73f --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0003_auto_20220814_1703.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2 on 2022-08-14 17:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0002_auto_20220814_0738'), + ] + + operations = [ + migrations.RenameModel( + old_name='ActivityImages', + new_name='ActivityImage', + ), + migrations.RenameModel( + old_name='ActivityMakingSteps', + new_name='ActivityMakingStep', + ), + migrations.RenameModel( + old_name='InspiringExamples', + new_name='InspiringExample', + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0004_rename_order_activitymakingstep_step_order.py b/zubhub_backend/zubhub/activities/migrations/0004_rename_order_activitymakingstep_step_order.py new file mode 100644 index 000000000..512d24499 --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0004_rename_order_activitymakingstep_step_order.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-08-14 19:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0003_auto_20220814_1703'), + ] + + operations = [ + migrations.RenameField( + model_name='activitymakingstep', + old_name='order', + new_name='step_order', + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0005_rename_image_url_image_file_url.py b/zubhub_backend/zubhub/activities/migrations/0005_rename_image_url_image_file_url.py new file mode 100644 index 000000000..2ee6eaddd --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0005_rename_image_url_image_file_url.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-08-16 09:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0004_rename_order_activitymakingstep_step_order'), + ] + + operations = [ + migrations.RenameField( + model_name='image', + old_name='image_url', + new_name='file_url', + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0006_activity_slug.py b/zubhub_backend/zubhub/activities/migrations/0006_activity_slug.py new file mode 100644 index 000000000..6622bd46a --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0006_activity_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2022-08-16 13:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0005_rename_image_url_image_file_url'), + ] + + operations = [ + migrations.AddField( + model_name='activity', + name='slug', + field=models.SlugField(default='first_activity_slug', max_length=1000, unique=True), + preserve_default=False, + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0007_auto_20220819_1640.py b/zubhub_backend/zubhub/activities/migrations/0007_auto_20220819_1640.py new file mode 100644 index 000000000..04c0bbe7e --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0007_auto_20220819_1640.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2 on 2022-08-19 16:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0006_activity_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='activity', + name='inspiring_artist', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inspiring_artist_activities', to='activities.inspiringartist'), + ), + migrations.AlterField( + model_name='activity', + name='materials_used_image', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='activities.image'), + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/0008_auto_20220829_1530.py b/zubhub_backend/zubhub/activities/migrations/0008_auto_20220829_1530.py new file mode 100644 index 000000000..6b0845f89 --- /dev/null +++ b/zubhub_backend/zubhub/activities/migrations/0008_auto_20220829_1530.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2022-08-29 15:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0007_auto_20220819_1640'), + ] + + operations = [ + migrations.AlterField( + model_name='inspiringartist', + name='name', + field=models.CharField(max_length=100, null=True), + ), + migrations.AlterField( + model_name='inspiringartist', + name='short_biography', + field=models.TextField(blank=True, max_length=10000, null=True), + ), + ] diff --git a/zubhub_backend/zubhub/activities/migrations/__init__.py b/zubhub_backend/zubhub/activities/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zubhub_backend/zubhub/activities/models.py b/zubhub_backend/zubhub/activities/models.py new file mode 100644 index 000000000..b68cc7030 --- /dev/null +++ b/zubhub_backend/zubhub/activities/models.py @@ -0,0 +1,138 @@ +import uuid +from django.db import models +from django.contrib.auth import get_user_model +from django.utils.text import slugify +from django.utils import timezone +from math import floor +from projects.models import Category + +Creator = get_user_model() + + +class Image(models.Model): + file_url = models.URLField(max_length=1000) + public_id = models.TextField(max_length=1000, blank=True) + + def __str__(self): + try: + image = self.file_url + except AttributeError: + image = '' + return "Photo <%s:%s>" % (self.public_id, image) + + +class InspiringArtist(models.Model): + '''this should be having more fields to distinguish an artist ''' + image = models.ForeignKey(Image, + on_delete=models.CASCADE, + null=True, + blank=True) + short_biography = models.TextField(max_length=10000, blank=True, null=True) + name = models.CharField(max_length=100, null=True) + + def __str__(self): + return self.name + + +class Activity(models.Model): + id = models.UUIDField(primary_key=True, + default=uuid.uuid4, + editable=False, + unique=True) + creators = models.ManyToManyField(Creator, + related_name="activities_created") + title = models.CharField(max_length=500) + learning_goals = models.TextField(max_length=10000, blank=True) + facilitation_tips = models.TextField(max_length=10000, blank=True) + motivation = models.TextField(max_length=10000, blank=True) + category = models.ForeignKey(Category, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="activities") + video = models.URLField(max_length=1000, blank=True, null=True) + materials_used = models.TextField(max_length=5000) + materials_used_image = models.ForeignKey(Image, + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + inspiring_artist = models.ForeignKey(InspiringArtist, + on_delete=models.SET_NULL, + null=True, + related_name="inspiring_artist_activities", + blank=True, + ) + views = models.ManyToManyField(Creator, + blank=True, + related_name="activities_viewed") + views_count = models.IntegerField(blank=True, default=0) + saved_count = models.IntegerField(blank=True, default=0) + saved_by = models.ManyToManyField(Creator, + blank=True, + related_name="activities_saved") + created_on = models.DateTimeField(default=timezone.now, null=True) + publish = models.BooleanField(default=False, null=True) + slug = models.SlugField(unique=True, max_length=1000) + + def save(self, *args, **kwargs): + if self.slug: + pass + else: + uid = str(uuid.uuid4()) + uid = uid[0:floor(len(uid) / 6)] + self.slug = slugify(self.title) + "-" + uid + + super().save(*args, **kwargs) + + def __str__(self): + return self.title + + +class InspiringExample(models.Model): + activity = models.ForeignKey(Activity, + on_delete=models.CASCADE, + null=True, + related_name="inspiring_examples", + blank=True) + description = models.TextField(max_length=10000, blank=True) + credit = models.TextField(max_length=1000, blank=True) + image = models.ForeignKey(Image, + on_delete=models.CASCADE, + null=True, + blank=True) + + def __str__(self): + return self.image + + +class ActivityImage(models.Model): + activity = models.ForeignKey(Activity, + on_delete=models.CASCADE, + null=True, + related_name="activity_images", + blank=True) + image = models.ForeignKey(Image, + on_delete=models.CASCADE, + null=True, + blank=True) + + def __str__(self): + return self.image + + +class ActivityMakingStep(models.Model): + activity = models.ForeignKey(Activity, + on_delete=models.CASCADE, + null=True, + related_name="making_steps", + blank=True) + image = models.ForeignKey(Image, + on_delete=models.CASCADE, + null=True, + blank=True) + description = models.TextField(max_length=10000, blank=True) + step_order = models.IntegerField() + + def __str__(self): + return self.description diff --git a/zubhub_backend/zubhub/activities/permissions.py b/zubhub_backend/zubhub/activities/permissions.py new file mode 100644 index 000000000..eff1fa057 --- /dev/null +++ b/zubhub_backend/zubhub/activities/permissions.py @@ -0,0 +1,22 @@ +from rest_framework.permissions import BasePermission + +class IsOwner(BasePermission): + message = "You must be the owner of this activity to perform this function" + + def has_object_permission(self, request, view, object): + return object.creators.filter(id=request.user.pk).exists() + + +class IsStaffOrModeratorOrEducator(BasePermission): + message = "You must be a staff a moderator or an educator to perform this function" + + def has_object_permission(self, request, view, object): + + return request.user.is_staff == True or request.user.tags.filter(name="moderator").exists() or request.user.tags.filter(name="educator").exists() + +class IsStaffOrModerator(BasePermission): + message = "You must be a staff a moderator to perform this function" + + def has_object_permission(self, request, view, object): + + return request.user.is_staff == True or request.user.tags.filter(name="moderator").exists() \ No newline at end of file diff --git a/zubhub_backend/zubhub/activities/serializers.py b/zubhub_backend/zubhub/activities/serializers.py new file mode 100644 index 000000000..2b2ccdd3d --- /dev/null +++ b/zubhub_backend/zubhub/activities/serializers.py @@ -0,0 +1,186 @@ + +from rest_framework import serializers +from django.contrib.auth import get_user_model +from .models import * +from projects.serializers import CategorySerializer, ProjectSerializer +from creators.serializers import CreatorMinimalSerializer +from .utils import * + +Creator = get_user_model() + + +class ImageSerializer(serializers.ModelSerializer): + + class Meta: + model = Image + fields = [ + "file_url", + "public_id" + ] + + +class InspiringArtistSerializer(serializers.ModelSerializer): + image = ImageSerializer(required=False) + + class Meta: + model = InspiringArtist + fields = [ + "id", + "name", + "short_biography", + "image" + ] + + +class ActivityImageSerializer(serializers.ModelSerializer): + image = ImageSerializer() + + class Meta: + model = ActivityImage + fields = [ + "image" + ] + + +class ActivityMakingStepSerializer(serializers.ModelSerializer): + image = ImageSerializer(required=False, allow_null=True) + step_order = serializers.IntegerField() + + class Meta: + model = ActivityMakingStep + fields = [ + "image", "description", "step_order" + ] + + +class InspiringExampleSerializer(serializers.ModelSerializer): + image = ImageSerializer() + + class Meta: + model = InspiringExample + fields = [ + "image", "description", "credit" + ] + + +class ActivitySerializer(serializers.ModelSerializer): + creators = CreatorMinimalSerializer(read_only=True, many=True) + saved_by = serializers.SlugRelatedField( + many=True, slug_field='id', read_only=True) + images = ActivityImageSerializer( + many=True, required=False, source="activity_images") + category = serializers.SlugRelatedField( + slug_field="name", queryset=Category.objects.all(), required=False) + created_on = serializers.DateTimeField(read_only=True) + views_count = serializers.IntegerField(read_only=True) + saved_count = serializers.IntegerField(read_only=True) + publish = serializers.BooleanField(required=False) + inspiring_artist = InspiringArtistSerializer(required=False) + making_steps = ActivityMakingStepSerializer(many=True, required=False) + inspiring_examples = InspiringExampleSerializer(many=True, required=False) + materials_used_image = ImageSerializer(required=False) + inspired_projects = ProjectSerializer(many=True, required=False) + + class Meta: + model = Activity + fields = [ + "id", + "inspired_projects", + "creators", + "title", + "motivation", + "images", + "video", + "materials_used", + "facilitation_tips", + "category", + "learning_goals", + "saved_by", + "views_count", + "saved_count", + "inspiring_artist", + "created_on", + "publish", + "making_steps", + "inspiring_examples", + "materials_used_image" + ] + + def create(self, validated_data): + if 'inspiring_artist' in validated_data: + validated_data['inspiring_artist'] = create_inspiring_artist( + validated_data.pop('inspiring_artist')) + if 'materials_used_image' in validated_data: + validated_data['materials_used_image'] = Image.objects.create( + **validated_data['materials_used_image']) + + activity_images = validated_data.pop('activity_images', None) + + making_steps = validated_data.pop('making_steps', None) + + inspiring_examples = validated_data.pop('inspiring_examples', None) + + activity = Activity.objects.create(**validated_data) + if making_steps: + create_making_steps(activity, making_steps) + if inspiring_examples: + create_inspiring_examples( + activity, inspiring_examples) + if activity_images: + create_activity_images(activity, activity_images) + activity.creators.add(self.context["request"].user) + return activity + + def update(self, activity, validated_data): + if (activity.inspiring_artist is not None or 'inspiring_artist' in validated_data): + if(activity.inspiring_artist is not None): + if('inspiring_artist' in validated_data): + if(activity.inspiring_artist.image is not None or validated_data['inspiring_artist'].get('image') is not None): + activity.inspiring_artist.image = update_image( + activity.inspiring_artist.image, validated_data['inspiring_artist'].get('image')) + + activity.inspiring_artist.name = validated_data['inspiring_artist'].get( + 'name') + activity.inspiring_artist.short_biography = validated_data[ + 'inspiring_artist'].get('short_biography') + activity.inspiring_artist.save() + + else: + activity.inspiring_artist.delete() + activity.inspiring_artist = None + else: + if('image' in validated_data['inspiring_artist']): + validated_data['inspiring_artist']['image'] = Image.objects.create( + **validated_data['inspiring_artist']['image']) + activity.inspiring_artist = InspiringArtist.objects.create( + **validated_data['inspiring_artist']) + + if(activity.materials_used_image is not None or 'materials_used_image' in validated_data): + if (activity.materials_used_image is None): + activity.materials_used_image = Image.objects.create( + **validated_data['materials_used_image']) + else: + if('materials_used_image' in validated_data): + image = {**validated_data['materials_used_image']} + activity.materials_used_image.file_url = image['file_url'] + activity.materials_used_image.public_id = image['public_id'] + activity.materials_used_image.save() + else: + activity.materials_used_image.delete() + activity.materials_used_image = None + if 'activity_images' in validated_data: + update_activity_images( + activity, validated_data.pop('activity_images')) + if 'making_steps' in validated_data: + update_making_steps(activity, validated_data.pop('making_steps')) + if 'inspiring_examples' in validated_data: + update_inspiring_examples( + activity, validated_data.pop('inspiring_examples')) + activity.title = validated_data.get('title') + activity.motivation = validated_data.get('motivation') + activity.facilitation_tips = validated_data.get('facilitation_tips') + activity.learning_goals = validated_data.get('learning_goals') + activity.materials_used = validated_data.get('materials_used') + activity.video = validated_data.get('video') + activity.save() + return activity diff --git a/zubhub_backend/zubhub/activities/tests.py b/zubhub_backend/zubhub/activities/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/zubhub_backend/zubhub/activities/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/zubhub_backend/zubhub/activities/urls.py b/zubhub_backend/zubhub/activities/urls.py new file mode 100644 index 000000000..bfd54246b --- /dev/null +++ b/zubhub_backend/zubhub/activities/urls.py @@ -0,0 +1,16 @@ +from django.urls import path +from .views import * + +app_name = "activities" + +urlpatterns = [ + path('', PublishedActivitiesAPIView.as_view(), name='index'), + path('unPublished', UnPublishedActivitiesAPIView.as_view(), name='unPublished'), + path('myActivities', UserActivitiesAPIView.as_view(), name='myActivities'), + path('create/', ActivityCreateAPIView.as_view(), name='create'), + path('/update/', ActivityUpdateAPIView.as_view(), name='update'), + path('/delete/', ActivityDeleteAPIView.as_view(), name='delete'), + path('/toggle-save/', ToggleSaveAPIView.as_view(), name='save'), + path('/toggle-publish/', togglePublishActivityAPIView.as_view(), name='publish') + +] diff --git a/zubhub_backend/zubhub/activities/utils.py b/zubhub_backend/zubhub/activities/utils.py new file mode 100644 index 000000000..1ec392985 --- /dev/null +++ b/zubhub_backend/zubhub/activities/utils.py @@ -0,0 +1,60 @@ +from .models import * + + +def create_inspiring_artist(inspiring_artist_data): + inspiring_artist_data['image'] = Image.objects.create( + **inspiring_artist_data['image']) + return InspiringArtist.objects.create( + **inspiring_artist_data) + + +def create_making_steps(activity, making_steps): + for step in making_steps: + if(step.get('image')): + saved_image = Image.objects.create(**step['image']) + step['image'] = saved_image + ActivityMakingStep.objects.create(activity=activity, **step) + + +def create_inspiring_examples(activity, inspiring_examples): + for example in inspiring_examples: + if 'image' in example: + saved_image = Image.objects.create(**example['image']) + example['image'] = saved_image + InspiringExample.objects.create(activity=activity, **example) + + +def create_activity_images(activity, images): + + for image in images: + saved_image = Image.objects.create(**image['image']) + ActivityImage.objects.create(activity=activity, image=saved_image) + + +def update_image(image, image_data): + if(image_data is not None and image is not None): + if image_data["file_url"] == image.file_url: + return image + else: + image.delete() + return Image.objects.create(**image_data) + else: + if(image): + image.delete() + else: + return Image.objects.create(**image_data) + + +def update_activity_images(activity, images_to_save): + ActivityImage.objects.filter(activity=activity).delete() + create_activity_images(activity, images_to_save) + + +def update_making_steps(activity, making_steps): + ActivityMakingStep.objects.filter(activity=activity).delete() + create_making_steps(activity, making_steps) + + +def update_inspiring_examples(activity, inspiring_examples): + InspiringExample.objects.filter(activity=activity).delete() + create_inspiring_examples(activity, inspiring_examples) diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py new file mode 100644 index 000000000..592561288 --- /dev/null +++ b/zubhub_backend/zubhub/activities/views.py @@ -0,0 +1,142 @@ +from django.shortcuts import render +from django.utils.translation import ugettext_lazy as _ +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.generics import ( + ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView) +from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny +from .permissions import IsStaffOrModeratorOrEducator, IsOwner, IsStaffOrModerator +from django.shortcuts import get_object_or_404 +from .models import * +from .serializers import * + + +class ActivityListAPIView(ListAPIView): + + serializer_class = ActivitySerializer + permission_classes = [AllowAny] + + def get_queryset(self): + all = Activity.objects.all() + return all + + +class UserActivitiesAPIView(ListAPIView): + """ + Fetch list of users activities. + Returns list of activities. + """ + + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsOwner] + + def get_queryset(self): + return self.request.user.activities_created.all() + + +class PublishedActivitiesAPIView(ListAPIView): + """ + Fetch list of published activities by any user. + Returns list of published activities. + """ + + serializer_class = ActivitySerializer + permission_classes = [AllowAny] + + def get_queryset(self): + return Activity.objects.filter(publish= True) + +class UnPublishedActivitiesAPIView(ListAPIView): + """ + Fetch list of unpublished activities by authenticated staff member. + + Requires authentication. + Returns list of unpublished activities. + """ + + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsStaffOrModerator] + + def get_queryset(self): + return Activity.objects.filter(publish= False) + +class ActivityCreateAPIView(CreateAPIView): + """ + Create new Activity.\n + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsStaffOrModeratorOrEducator] + +class ActivityUpdateAPIView(UpdateAPIView): + """ + Update activity. + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsOwner] + +class ActivityDeleteAPIView(DestroyAPIView): + """ + Delete a activity and related objects from database. + + Requires authentication. + Requires activity id. + Returns {details: "ok"} + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsOwner] + + def delete(self, request, *args, **kwargs): + activity = self.get_object() + if activity: + result = self.destroy(request, *args, **kwargs) + request.user.save() + return result + + +class ToggleSaveAPIView(RetrieveAPIView): + """ + Add/Remove an activity from authenticated user's bookmark. + + Requires authentication. + Requires activity id. + Returns activity details. + """ + + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated] + + def get_object(self): + pk = self.kwargs.get("pk") + obj = get_object_or_404(self.get_queryset(), pk=pk) + + if self.request.user in obj.saved_by.all(): + obj.saved_by.remove(self.request.user) + obj.save() + else: + obj.saved_by.add(self.request.user) + obj.save() + return obj + + +class togglePublishActivityAPIView(RetrieveAPIView): + """ + publish an activity. + Requires activity id. + Returns updated activity. + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + permission_classes = [IsAuthenticated, IsStaffOrModerator] + + + def get_object(self): + + pk = self.kwargs.get("pk") + obj = get_object_or_404(self.get_queryset(), pk=pk) + obj.publish = not obj.publish + obj.save() + return obj diff --git a/zubhub_backend/zubhub/activitylog/__init__.py b/zubhub_backend/zubhub/activitylog/__init__.py new file mode 100644 index 000000000..36f14953a --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/__init__.py @@ -0,0 +1 @@ +default_app_config = 'activitylog.apps.ActivitylogConfig' \ No newline at end of file diff --git a/zubhub_backend/zubhub/activitylog/admin.py b/zubhub_backend/zubhub/activitylog/admin.py new file mode 100644 index 000000000..bb0d21c49 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +# Register your models here. +from .models import Activitylog + +admin.site.register(Activitylog) diff --git a/zubhub_backend/zubhub/activitylog/apps.py b/zubhub_backend/zubhub/activitylog/apps.py new file mode 100644 index 000000000..86cde35fe --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ActivitylogConfig(AppConfig): + name = 'activitylog' diff --git a/zubhub_backend/zubhub/activitylog/migrations/0001_initial.py b/zubhub_backend/zubhub/activitylog/migrations/0001_initial.py new file mode 100644 index 000000000..c85c27c56 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2 on 2022-08-12 15:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Activitylog', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('type', models.PositiveSmallIntegerField(choices=[(1, 'Clap'), (2, 'Comment'), (3, 'Follow'), (4, 'Bookmark')])), + ('message', models.CharField(blank=True, max_length=255, null=True)), + ('link', models.CharField(blank=True, max_length=1000, null=True)), + ('date', models.DateTimeField(default=django.utils.timezone.now)), + ('source', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activitylog_source', to=settings.AUTH_USER_MODEL)), + ('target', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='activitylog_target', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/zubhub_backend/zubhub/activitylog/migrations/__init__.py b/zubhub_backend/zubhub/activitylog/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zubhub_backend/zubhub/activitylog/models.py b/zubhub_backend/zubhub/activitylog/models.py new file mode 100644 index 000000000..27f4e195a --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/models.py @@ -0,0 +1,27 @@ +from django.utils import timezone +from django.db import models +import uuid + +from creators.models import Creator + + +class Activitylog(models.Model): + class Type(models.IntegerChoices): + CLAP = 1 + COMMENT = 2 + FOLLOW = 3 + BOOKMARK = 4 + + id = models.UUIDField( + primary_key=True, default=uuid.uuid4, editable=False, unique=True) + type = models.PositiveSmallIntegerField( + choices=Type.choices, + ) + target = models.ForeignKey( + Creator, on_delete=models.CASCADE, null=True, related_name="activitylog_target", blank=True) + source = models.ForeignKey( + Creator, on_delete=models.CASCADE, null=True, related_name="activitylog_source", blank=True) + message = models.CharField(max_length=255, blank=True, null=True) + link = models.CharField(max_length=1000, blank=True, null=True) + date = models.DateTimeField(default=timezone.now) + diff --git a/zubhub_backend/zubhub/activitylog/pagination.py b/zubhub_backend/zubhub/activitylog/pagination.py new file mode 100644 index 000000000..6ede3bd88 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/pagination.py @@ -0,0 +1,6 @@ +from rest_framework.pagination import PageNumberPagination + + +class ActivitylogNumberPagination(PageNumberPagination): + page_size = 10 + \ No newline at end of file diff --git a/zubhub_backend/zubhub/activitylog/permissions.py b/zubhub_backend/zubhub/activitylog/permissions.py new file mode 100644 index 000000000..44e1745ad --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/permissions.py @@ -0,0 +1,12 @@ +from rest_framework.throttling import UserRateThrottle +from django.conf import settings + + +class SustainedRateThrottle(UserRateThrottle): + scope = 'sustained' + + def allow_request(self, request, view): + if settings.DEBUG: + return True + return super().allow_request(request, view) + \ No newline at end of file diff --git a/zubhub_backend/zubhub/activitylog/serializers.py b/zubhub_backend/zubhub/activitylog/serializers.py new file mode 100644 index 000000000..f97a2e258 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/serializers.py @@ -0,0 +1,30 @@ +from creators.models import Creator +from rest_framework import serializers + +from .models import Activitylog +from creators.serializers import CreatorMinimalSerializer + +class CreatorInfoSerializer(serializers.ModelSerializer): + tags = serializers.SlugRelatedField(slug_field="name", + read_only=True, + many=True) + + class Meta: + model = Creator + fields = ('id', 'username', 'followers_count', + 'following_count', 'projects_count', "tags", + ) + + +class ActivitylogSerializer(serializers.ModelSerializer): + + source = CreatorInfoSerializer() + target = CreatorInfoSerializer() + + class Meta: + model = Activitylog + fields = ('id', 'source', 'target', 'date', 'type', 'link', 'message') + + read_only_field = [ + 'id', 'source', 'target', 'date', 'type', 'link', 'message' + ] diff --git a/zubhub_backend/zubhub/activitylog/tests.py b/zubhub_backend/zubhub/activitylog/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/zubhub_backend/zubhub/activitylog/urls.py b/zubhub_backend/zubhub/activitylog/urls.py new file mode 100644 index 000000000..559dcc639 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import * + +app_name = "activitylog" + +urlpatterns = [ + path('',AllUsersActivitylogAPIView.as_view(), name='all-activitylogs' ), + path('/', + UserActivitylogAPIView.as_view(), name='user-activitylog') +] diff --git a/zubhub_backend/zubhub/activitylog/utils.py b/zubhub_backend/zubhub/activitylog/utils.py new file mode 100644 index 000000000..4431cfb86 --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/utils.py @@ -0,0 +1,22 @@ +from datetime import datetime, timedelta +from email import message +from activitylog.models import Activitylog +from django.template.loader import render_to_string + + +def get_activity_template_name(activity_type): + file_extension='.html' + return f'activitylog/{activity_type.name.lower()}{file_extension}' + + +def push_activity(user, source, context, activity_type, link): + template_name = get_activity_template_name(activity_type) + + message= render_to_string(template_name, context) + + activity= Activitylog.objects.create(type= activity_type, target= user, + source= source, message= message, + link= link) + + activity.save() + \ No newline at end of file diff --git a/zubhub_backend/zubhub/activitylog/views.py b/zubhub_backend/zubhub/activitylog/views.py new file mode 100644 index 000000000..4f4d09b3d --- /dev/null +++ b/zubhub_backend/zubhub/activitylog/views.py @@ -0,0 +1,28 @@ +from .serializers import ActivitylogSerializer +from .pagination import ActivitylogNumberPagination +from .permissions import SustainedRateThrottle +from rest_framework.permissions import IsAuthenticated + +from .models import Activitylog + +from rest_framework.generics import ListAPIView +from django.utils.translation import ugettext_lazy as _ + +from rest_framework.generics import (ListAPIView) + +# Create your views here. + +class AllUsersActivitylogAPIView(ListAPIView): + serializer_class = ActivitylogSerializer + pagination_class = ActivitylogNumberPagination + + def get_queryset(self): + return Activitylog.objects.all() + +class UserActivitylogAPIView(ListAPIView): + serializer_class = ActivitylogSerializer + pagination_class = ActivitylogNumberPagination + + def get_queryset(self): + username = self.kwargs.get("username") + return Activitylog.objects.all().filter(source__username= username).order_by("-date") diff --git a/zubhub_backend/zubhub/creators/admin.py b/zubhub_backend/zubhub/creators/admin.py index 6a0ed5eaf..58f6fe097 100644 --- a/zubhub_backend/zubhub/creators/admin.py +++ b/zubhub_backend/zubhub/creators/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin from django.db import transaction -from .models import PhoneNumber, Setting, CreatorGroup, CreatorTag +from .models import PhoneNumber, Setting, CreatorGroup, CreatorTag, Badge from .utils import (send_group_invite_notification, custom_set_creatortags_queryset, @@ -21,6 +21,13 @@ def tags(obj): return ", ".join(tags) return None +def badges(obj): + if obj: + badges = [] + for badge in obj.badges.all(): + badges.append(badge.badge_title) + return ", ". join(badges) + return None def group_projects(obj): if obj: @@ -142,7 +149,10 @@ class CreatorAdmin(UserAdmin): ('followers',), ('followers_count',), ('following_count',), - ('projects_count',) + ('projects_count',), + ('total_likes',), + ('total_views',), + ('badges',), ) }), ('Important Dates', { @@ -160,9 +170,9 @@ class CreatorAdmin(UserAdmin): ) - list_display = UserAdmin.list_display + (tags, active,) + list_display = UserAdmin.list_display + (tags, active, badges) list_per_page = 50 ## paginate when more than 50 items - readonly_fields = UserAdmin.readonly_fields + ('avatar',) + ('followers_count',) + ('following_count',) + ('projects_count',) + readonly_fields = UserAdmin.readonly_fields + ('avatar',) + ('followers_count',) + ('following_count',) + ('projects_count',) + ('total_likes',) + ('total_views',) actions = ["activate_creators", "deactivate_creators"] ## disable the ability to add a new creator from the admin for now. @@ -211,10 +221,22 @@ def save_model(self, request, obj, form, change): super(CreatorAdmin, self).save_model( request, obj, form, change) +class BadgeAdmin(admin.ModelAdmin): + list_display= ["badge_title", "type", "threshold_value", "id", used_by] + fields = ('type', 'badge_title', 'threshold_value', 'id',) + readonly_fields= ( 'id', 'type',) + + def has_delete_permission(self, request, obj=None): + # Disable delete + return False + # disable the ability to add a new badge from the admin for now. + def has_add_permission(self, request, obj=None): + return False admin.site.register(Creator, CreatorAdmin) admin.site.register(PhoneNumber, PhoneNumberAdmin) admin.site.register(Setting, SettingAdmin) admin.site.register(CreatorGroup, CreatorGroupAdmin) admin.site.register(CreatorTag, CreatorTagAdmin) +admin.site.register(Badge, BadgeAdmin) diff --git a/zubhub_backend/zubhub/creators/management/commands/initial_badges.txt b/zubhub_backend/zubhub/creators/management/commands/initial_badges.txt new file mode 100644 index 000000000..40b1a8461 --- /dev/null +++ b/zubhub_backend/zubhub/creators/management/commands/initial_badges.txt @@ -0,0 +1,14 @@ +Hatchling:0:1 +Flying Bird:10:1 +Master of the sky:50:1 +Expert Builder:100:1 +Helping Hand:10:2 +Always Available:500:2 +Expert Advisor:1000:2 +Getting Famous:10:3 +Person of Interest:100:3 +Popular Projects:5000:3 +Idea Factory:100000:3 +Interesting Projects:10:4 +Favourite Kid:500:4 +Captain Projects:1000:4 diff --git a/zubhub_backend/zubhub/creators/management/commands/populate_initial_badges.py b/zubhub_backend/zubhub/creators/management/commands/populate_initial_badges.py new file mode 100644 index 000000000..ad67935f4 --- /dev/null +++ b/zubhub_backend/zubhub/creators/management/commands/populate_initial_badges.py @@ -0,0 +1,22 @@ +from creators.models import Badge +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = 'Populate Badge table with initial badges' + + def handle(self, *args, **kwargs): + if Badge.objects.all().count() == 0: + with open("./zubhub/creators/management/commands/initial_badges.txt", "r") as badges: + badges = badges.readlines() + for badge in badges: + remove_col=badge.split(':') + remove_col[2]=remove_col[2].split('\n')[0] + title=remove_col[0] + value=int(remove_col[1]) + category_type=int(remove_col[2]) + Badge.objects.create(badge_title = title, threshold_value=value, + type= category_type ) + self.stdout.write(self.style.SUCCESS('The Badge table has been successfully populated!')) + else: + self.stdout.write(self.style.NOTICE('The Badge table is already populated')) diff --git a/zubhub_backend/zubhub/creators/migrations/0009_alter_setting_contact.py b/zubhub_backend/zubhub/creators/migrations/0009_alter_setting_contact.py new file mode 100644 index 000000000..8d3f10661 --- /dev/null +++ b/zubhub_backend/zubhub/creators/migrations/0009_alter_setting_contact.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-08-09 13:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('creators', '0008_auto_20220222_0254'), + ] + + operations = [ + migrations.AlterField( + model_name='setting', + name='contact', + field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'WHATSAPP'), (2, 'EMAIL'), (3, 'SMS'), (4, 'WEB')], default=3, null=True), + ), + ] diff --git a/zubhub_backend/zubhub/creators/migrations/0009_auto_20220822_0842.py b/zubhub_backend/zubhub/creators/migrations/0009_auto_20220822_0842.py new file mode 100644 index 000000000..c4fcebe44 --- /dev/null +++ b/zubhub_backend/zubhub/creators/migrations/0009_auto_20220822_0842.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2 on 2022-08-22 08:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('creators', '0008_auto_20220222_0254'), + ] + + operations = [ + migrations.CreateModel( + name='Badge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.PositiveSmallIntegerField(choices=[(1, 'Projects'), (2, 'Comments'), (3, 'Views'), (4, 'Likes')])), + ('threshold_value', models.IntegerField(blank=True, default=0)), + ('badge_title', models.CharField(default='', max_length=225)), + ], + ), + migrations.AlterField( + model_name='setting', + name='contact', + field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'WHATSAPP'), (2, 'EMAIL'), (3, 'SMS'), (4, 'WEB')], default=3, null=True), + ), + migrations.AddField( + model_name='creator', + name='badges', + field=models.ManyToManyField(blank=True, related_name='creators', to='creators.Badge'), + ), + ] diff --git a/zubhub_backend/zubhub/creators/migrations/0010_merge_20220921_2226.py b/zubhub_backend/zubhub/creators/migrations/0010_merge_20220921_2226.py new file mode 100644 index 000000000..a4cf65d01 --- /dev/null +++ b/zubhub_backend/zubhub/creators/migrations/0010_merge_20220921_2226.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2022-09-21 22:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('creators', '0009_alter_setting_contact'), + ('creators', '0009_auto_20220822_0842'), + ] + + operations = [ + ] diff --git a/zubhub_backend/zubhub/creators/models.py b/zubhub_backend/zubhub/creators/models.py index ff9a644d1..4ddac140e 100644 --- a/zubhub_backend/zubhub/creators/models.py +++ b/zubhub_backend/zubhub/creators/models.py @@ -13,6 +13,7 @@ from .tasks import update_creator_tag_index_task from .managers import PhoneNumberManager from .model_utils import user_phone +from django.db.models import Sum try: from allauth.account import app_settings as allauth_settings @@ -54,6 +55,21 @@ def save(self, *args, **kwargs): update_creator_tag_index_task.delay() super().save(*args, **kwargs) +class Badge(models.Model): + class Type(models.IntegerChoices): + PROJECTS = 1 + COMMENTS = 2 + VIEWS = 3 + LIKES = 4 + + type = models.PositiveSmallIntegerField( + choices=Type.choices, + ) + threshold_value = models.IntegerField(blank=True, default=0) + badge_title = models.CharField(blank=False, default="", max_length=225) + + def __str__(self): + return self.badge_title class Creator(AbstractUser): @@ -72,10 +88,20 @@ class Creator(AbstractUser): projects_count = models.IntegerField(blank=True, default=0) tags = models.ManyToManyField(CreatorTag, blank=True, related_name="creators") search_vector = SearchVectorField(null=True) - + badges = models.ManyToManyField(Badge, blank=True, related_name="creators" ) class Meta: indexes = (GinIndex(fields=["search_vector"]),) + @property + def total_likes(self): + total_likes= self.projects.aggregate(Sum("likes_count"))["likes_count__sum"] + return total_likes + + @property + def total_views(self): + total_views= self.projects.aggregate(Sum("views_count"))["views_count__sum"] + return total_views + def save(self, *args, **kwargs): if not self.avatar: self.avatar = 'https://robohash.org/{0}'.format(self.username) diff --git a/zubhub_backend/zubhub/creators/serializers.py b/zubhub_backend/zubhub/creators/serializers.py index b1b111447..53e4401e9 100644 --- a/zubhub_backend/zubhub/creators/serializers.py +++ b/zubhub_backend/zubhub/creators/serializers.py @@ -3,6 +3,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from django.contrib.auth import get_user_model + +from .admin import badges from .models import Location, PhoneNumber from allauth.account.models import EmailAddress from rest_auth.registration.serializers import RegisterSerializer @@ -25,14 +27,17 @@ class CreatorMinimalSerializer(serializers.ModelSerializer): tags = serializers.SlugRelatedField(slug_field="name", read_only=True, many=True) + badges = serializers.SlugRelatedField(slug_field="badge_title", + read_only=True, + many=True) class Meta: model = Creator fields = ('id', 'username', 'avatar', 'comments', 'bio', 'followers', - 'following_count', 'projects_count', 'members_count', 'tags') + 'following_count', 'projects_count', 'members_count', 'tags', 'badges') - read_only_fields = ["id", "projects_count", "following_count", "tags"] + read_only_fields = ["id", "projects_count", "following_count", "tags", "badges"] def get_members_count(self, obj): if hasattr(obj, "creatorgroup"): @@ -80,16 +85,18 @@ class CreatorSerializer(CreatorMinimalSerializer): tags = serializers.SlugRelatedField(slug_field="name", read_only=True, many=True) - + badges = serializers.SlugRelatedField(slug_field="badge_title", + read_only=True, + many=True) class Meta: model = Creator fields = ('id', 'username', 'email', 'phone', 'avatar', 'location', 'comments', 'dateOfBirth', 'bio', 'followers', - 'following_count', 'projects_count', 'members_count', 'tags') + 'following_count', 'projects_count', 'members_count', 'tags', 'badges') read_only_fields = [ - "id", "projects_count", "following_count", "dateOfBirth", "tags" + "id", "projects_count", "following_count", "dateOfBirth", "tags", "badges" ] def validate_email(self, email): diff --git a/zubhub_backend/zubhub/creators/utils.py b/zubhub_backend/zubhub/creators/utils.py index b551cf612..b035cf45b 100644 --- a/zubhub_backend/zubhub/creators/utils.py +++ b/zubhub_backend/zubhub/creators/utils.py @@ -10,9 +10,13 @@ from creators.tasks import upload_file_task, send_mass_email, send_mass_text, send_whatsapp from creators.models import Setting from notifications.models import Notification +from activitylog.models import Activitylog +from activitylog.utils import push_activity from notifications.utils import push_notification, get_notification_template_name -from creators.models import Creator +from creators.models import Creator, Badge +from projects.models import Project, Comment from django.template.loader import render_to_string +from django.db.models import Sum try: from allauth.account.adapter import get_adapter @@ -367,6 +371,12 @@ def enforce_creator__creator_tags_constraints(creator, tag): else: return creator.tags.all() +def activity_log(target: List[Creator], source: Creator, contexts, activity_type: Activitylog.Type, link: str): + for user, context in zip(target, contexts): + context.update({'source': source.username}) + context.update({'target': user.username}) + + push_activity(user, source, context, activity_type, link) enabled_notification_settings: Dict[Notification.Type, Set[int]] = { @@ -456,3 +466,57 @@ def send_notification(users: List[Creator], source: Creator, contexts, # EmailAddress.objects.create( # user=user, email=email, primary=False, verified=False # ) + + +def remove_initial_titles(creator, ids): + for id in ids: + creator.badges.remove(Badge.objects.get(id= id)) + +def add_titles(creator, count, ids): + for id in ids: + value = Badge.objects.get(id= id).threshold_value + if count > value: + creator.badges.add(Badge.objects.get(id = id)) + break + +def set_badge_like_category(creator): + likes_count = (Project.objects.filter(creator= creator) + .aggregate(Sum(('likes_count')))["likes_count__sum"]) + + badge_ids = [14, 13, 12] + + remove_initial_titles(creator, badge_ids) + add_titles(creator, likes_count, badge_ids) + +def set_badge_view_category(creator): + views_count = (Project.objects.filter(creator= creator).aggregate( + Sum(('views_count')))["views_count__sum"]) + + badge_ids = [11, 10, 9, 8] + + remove_initial_titles(creator, badge_ids) + add_titles(creator, views_count, badge_ids) + +def set_badge_comment_category(creator): + creator_id = creator.id + comments_count = Comment.objects.filter(creator__id= creator_id).count() + + badge_ids = [7, 6, 5] + + remove_initial_titles(creator, badge_ids) + add_titles(creator, comments_count, badge_ids) + +def set_badge_project_category(creator, projects_count): + project_count_before_del = creator.projects_count + + if(projects_count < project_count_before_del): + queryset = Project.objects.filter(creator= creator) + if( queryset.exists()): + set_badge_view_category(creator) + set_badge_like_category(creator) + set_badge_comment_category(creator) + + badge_ids = [4, 3, 2, 1] + + remove_initial_titles(creator, badge_ids) + add_titles(creator, projects_count, badge_ids) diff --git a/zubhub_backend/zubhub/creators/views.py b/zubhub_backend/zubhub/creators/views.py index 3686a1d19..0a9e2fd28 100644 --- a/zubhub_backend/zubhub/creators/views.py +++ b/zubhub_backend/zubhub/creators/views.py @@ -2,6 +2,7 @@ from io import StringIO from django.utils.translation import ugettext_lazy as _ from notifications.models import Notification +from activitylog.models import Activitylog from rest_framework import status from django.http import Http404 from django.contrib.auth import get_user_model @@ -39,7 +40,7 @@ from .pagination import CreatorNumberPagination from .utils import (perform_send_phone_confirmation, perform_send_email_confirmation, process_avatar, - send_group_invite_notification, perform_creator_search, send_notification) + send_group_invite_notification, perform_creator_search, send_notification, activity_log) from .permissions import IsOwner Creator = get_user_model() @@ -396,6 +397,13 @@ def get_object(self): obj.save() send_notification([obj], self.request.user, [{}], Notification.Type.FOLLOW, f'/creators/{self.request.user.username}') + activity_log( + [obj], + self.request.user, + [{}], + Activitylog.Type.FOLLOW, + f'/creators/{obj}' + ) self.request.user.save() return obj diff --git a/zubhub_backend/zubhub/projects/migrations/0007_tag_tag_name_gin_idx.py b/zubhub_backend/zubhub/projects/migrations/0007_tag_tag_name_gin_idx.py new file mode 100644 index 000000000..3d3041c02 --- /dev/null +++ b/zubhub_backend/zubhub/projects/migrations/0007_tag_tag_name_gin_idx.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-11-07 04:01 + +import django.contrib.postgres.indexes +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0006_auto_20220409_1917'), + ] + + operations = [ + migrations.AddIndex( + model_name='tag', + index=django.contrib.postgres.indexes.GinIndex(fields=['name'], name='tag_name_gin_idx', opclasses=['gin_trgm_ops']), + ), + ] diff --git a/zubhub_backend/zubhub/projects/migrations/0008_project_inspired_from_activity.py b/zubhub_backend/zubhub/projects/migrations/0008_project_inspired_from_activity.py new file mode 100644 index 000000000..c36f17f59 --- /dev/null +++ b/zubhub_backend/zubhub/projects/migrations/0008_project_inspired_from_activity.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2 on 2022-08-11 11:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('activities', '0001_initial'), + ('projects', '0007_tag_tag_name_gin_idx'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='inspired_from_activity', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inspired_projects', to='activities.activity'), + ), + ] diff --git a/zubhub_backend/zubhub/projects/models.py b/zubhub_backend/zubhub/projects/models.py index 588ed561f..8ca858971 100644 --- a/zubhub_backend/zubhub/projects/models.py +++ b/zubhub_backend/zubhub/projects/models.py @@ -8,6 +8,7 @@ from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex from projects.utils import clean_comment_text, clean_project_desc +#from activities.models import Activity Creator = get_user_model() @@ -78,7 +79,7 @@ class Project(models.Model): description = models.CharField(max_length=10000, blank=True, null=True) video = models.URLField(max_length=1000, blank=True, null=True) materials_used = models.CharField(max_length=5000) - category = models.ForeignKey(Category, + category = models.ForeignKey("projects.Category", on_delete=models.SET_NULL, null=True, blank=True, @@ -102,7 +103,11 @@ class Project(models.Model): on_delete=models.RESTRICT, related_name='project_target') search_vector = SearchVectorField(null=True) - + inspired_from_activity = models.ForeignKey("activities.Activity", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="inspired_projects") class Meta: indexes = (GinIndex(fields=["search_vector"]), ) diff --git a/zubhub_backend/zubhub/projects/serializers.py b/zubhub_backend/zubhub/projects/serializers.py index 996dd7fe0..4713bc1e8 100644 --- a/zubhub_backend/zubhub/projects/serializers.py +++ b/zubhub_backend/zubhub/projects/serializers.py @@ -11,13 +11,14 @@ from .utils import update_images, update_tags, parse_comment_trees, get_published_projects_for_user from .tasks import update_video_url_if_transform_ready, delete_file_task from math import ceil - +from activities.models import Activity Creator = get_user_model() class PublishingRuleSerializer(serializers.ModelSerializer): visible_to = CreatorMinimalSerializer(read_only=True, many=True) + class Meta: model = PublishingRule fields = [ @@ -25,6 +26,7 @@ class Meta: "visible_to" ] + class CommentSerializer(serializers.ModelSerializer): id = serializers.UUIDField(read_only=True) creator = serializers.SerializerMethodField('get_creator') @@ -80,6 +82,7 @@ class Meta: "name" ] + class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category @@ -102,7 +105,9 @@ class ProjectSerializer(serializers.ModelSerializer): slug_field="name", queryset=Category.objects.all(), required=False) created_on = serializers.DateTimeField(read_only=True) views_count = serializers.IntegerField(read_only=True) - publish = PublishingRuleSerializer(read_only=True) + publish = PublishingRuleSerializer(read_only=True) + activity = serializers.SlugRelatedField( + slug_field="id", queryset=Activity.objects.all(), required=False) class Meta: model = Project @@ -121,7 +126,8 @@ class Meta: "views_count", "comments", "created_on", - "publish" + "publish", + "activity" ] read_only_fields = ["created_on", "views_count"] @@ -190,8 +196,8 @@ def validate_publish(self, publish): try: if publish["type"] not in [ - PublishingRule.DRAFT, PublishingRule.PUBLIC, - PublishingRule.AUTHENTICATED_VIEWERS, PublishingRule.PREVIEW]: + PublishingRule.DRAFT, PublishingRule.PUBLIC, + PublishingRule.AUTHENTICATED_VIEWERS, PublishingRule.PREVIEW]: raise serializers.ValidationError( _("publish type is not supported. must be one of 1,2,3 or 4") ) @@ -202,35 +208,38 @@ def validate_publish(self, publish): ) if (publish["type"] == PublishingRule.PREVIEW and - len(publish["visible_to"]) == 0): + len(publish["visible_to"]) == 0): raise serializers.ValidationError( _("publish visible_to must not be empty when publish type is Preview") ) except: - raise serializers.ValidationError( - _("publish format is not supported") - ) + raise serializers.ValidationError( + _("publish format is not supported") + ) return publish - def create(self, validated_data): images_data = validated_data.pop('images') - tags_data = self.validate_tags(self.context['request'].data.get("tags", [])) + tags_data = self.validate_tags( + self.context['request'].data.get("tags", [])) category = None + activity = validated_data.pop('activity', None) if validated_data.get('category', None): category = validated_data.pop('category') - publish = self.validate_publish(self.context['request'].data.get("publish", {})) + publish = self.validate_publish( + self.context['request'].data.get("publish", {})) with transaction.atomic(): - rule = PublishingRule.objects.create(type=publish["type"], - publisher_id=str(self.context["request"].user.id)) - + rule = PublishingRule.objects.create(type=publish["type"], + publisher_id=str(self.context["request"].user.id)) + if rule.type == PublishingRule.PREVIEW: - rule.visible_to.set(Creator.objects.filter(username__in=publish["visible_to"])) + rule.visible_to.set(Creator.objects.filter( + username__in=publish["visible_to"])) project = Project.objects.create(**validated_data, publish=rule) @@ -243,6 +252,8 @@ def create(self, validated_data): if category: category.projects.add(project) + if activity is not None: + activity.inspired_projects.add(project) if project.video.find("cloudinary.com") > -1 and project.video.split(".")[-1] != "mpd": update_video_url_if_transform_ready.delay( @@ -263,25 +274,26 @@ def update(self, project, validated_data): if (project.video.find("cloudinary.com") > -1 or project.video.startswith("{0}://{1}".format( - settings.DEFAULT_MEDIA_SERVER_PROTOCOL, - settings.DEFAULT_MEDIA_SERVER_DOMAIN)) and - project.video != video_data): + settings.DEFAULT_MEDIA_SERVER_PROTOCOL, + settings.DEFAULT_MEDIA_SERVER_DOMAIN)) and + project.video != video_data): delete_file_task.delay(project.video) with transaction.atomic(): - rule = PublishingRule.objects.create(type=publish["type"], - publisher_id=str(self.context["request"].user.id)) + rule = PublishingRule.objects.create(type=publish["type"], + publisher_id=str(self.context["request"].user.id)) if rule.type == PublishingRule.PREVIEW: - rule.visible_to.set(Creator.objects.filter(username__in=publish["visible_to"])) + rule.visible_to.set(Creator.objects.filter( + username__in=publish["visible_to"])) project.title = validated_data.pop("title") project.description = validated_data.pop("description") project.video = video_data project.materials_used = validated_data.pop("materials_used") project.publish = rule - + project.save() update_images(project, images_data) @@ -308,7 +320,7 @@ class ProjectListSerializer(serializers.ModelSerializer): saved_by = serializers.SlugRelatedField( many=True, slug_field='id', read_only=True) created_on = serializers.DateTimeField(read_only=True) - publish = PublishingRuleSerializer(read_only=True) + publish = PublishingRuleSerializer(read_only=True) class Meta: model = Project @@ -345,8 +357,8 @@ def paginated_projects(self, obj): projects = obj.projects.all() projects = get_published_projects_for_user( - self.context['request'].user, - projects) + self.context['request'].user, + projects) paginator = ProjectNumberPagination() page = paginator.paginate_queryset( diff --git a/zubhub_backend/zubhub/projects/views.py b/zubhub_backend/zubhub/projects/views.py index a6ffe2cdc..1459f7dbb 100644 --- a/zubhub_backend/zubhub/projects/views.py +++ b/zubhub_backend/zubhub/projects/views.py @@ -15,11 +15,15 @@ from projects.permissions import (IsOwner, IsStaffOrModerator, SustainedRateThrottle, PostUserRateThrottle, GetUserRateThrottle, CustomUserRateThrottle) +from activitylog.models import Activitylog from .models import Project, Comment, StaffPick, Category, Tag, PublishingRule +from creators.models import Creator from .utils import (ProjectSearchCriteria, project_changed, detect_mentions, perform_project_search, can_view, get_published_projects_for_user) -from creators.utils import activity_notification, send_notification +from creators.utils import (activity_notification, send_notification, activity_log, set_badge_like_category, + set_badge_project_category, set_badge_view_category, + set_badge_comment_category) from .serializers import (ProjectSerializer, ProjectListSerializer, CommentSerializer, CategorySerializer, TagSerializer, StaffPickSerializer) @@ -56,6 +60,10 @@ def perform_create(self, serializer): obj = serializer.save(creator=self.request.user) self.request.user.save() + creator = Creator.objects.get(id = obj.creator_id) + project_count= creator.projects_count + set_badge_project_category(creator, project_count) + if self.request.user.followers is not None: send_notification( list(self.request.user.followers.all()), @@ -130,9 +138,12 @@ class ProjectDeleteAPIView(DestroyAPIView): def delete(self, request, *args, **kwargs): project = self.get_object() + creator = Creator.objects.get(id = project.creator_id) if project: result = self.destroy(request, *args, **kwargs) + project_count_after_deletion = creator.projects_count -1 request.user.save() + set_badge_project_category(creator, project_count_after_deletion) return result @@ -272,7 +283,8 @@ def get_object(self): obj.views.add(self.request.user) obj.views_count += 1 obj.save() - + creator = Creator.objects.get(id = obj.creator_id) + set_badge_view_category(creator) return obj else: raise PermissionDenied( @@ -335,6 +347,17 @@ def get_object(self): f'/projects/{obj.pk}' ) + activity_log( + [obj.creator], + self.request.user, + [{'project': obj.title}], + Activitylog.Type.CLAP, + f'/projects/{obj.pk}' + ) + + creator = Creator.objects.get(id = obj.creator_id) + set_badge_like_category(creator) + return obj else: raise PermissionDenied( @@ -377,6 +400,15 @@ def get_object(self): f'/projects/{obj.pk}' ) + activity_log( + [obj.creator], + self.request.user, + [{'project': obj.title}], + Activitylog.Type.BOOKMARK, + f'/projects/{obj.pk}' + ) + + return obj else: raise PermissionDenied( @@ -446,6 +478,11 @@ def create(self, request, *args, **kwargs): result = self.get_object() + creator_str= self.request.user.username + creator_id= Creator.objects.get(username= creator_str).id + creator= Creator.objects.get(id = creator_id) + set_badge_comment_category(creator) + if result: detect_mentions({ "text": text, @@ -460,6 +497,14 @@ def create(self, request, *args, **kwargs): f'/projects/{result.pk}' ) + activity_log( + [result.creator], + self.request.user, + [{'project': result.title}], + Activitylog.Type.COMMENT, + f'/projects/{result.pk}' + ) + return Response(ProjectSerializer(result, context={ 'request': request }).data, diff --git a/zubhub_backend/zubhub/templates/activitylog/bookmark.html b/zubhub_backend/zubhub/templates/activitylog/bookmark.html new file mode 100644 index 000000000..16144f574 --- /dev/null +++ b/zubhub_backend/zubhub/templates/activitylog/bookmark.html @@ -0,0 +1 @@ +{{ source }} bookmarked {{target}}'s project "{{ project }}" diff --git a/zubhub_backend/zubhub/templates/activitylog/clap.html b/zubhub_backend/zubhub/templates/activitylog/clap.html new file mode 100644 index 000000000..edbf70e7a --- /dev/null +++ b/zubhub_backend/zubhub/templates/activitylog/clap.html @@ -0,0 +1 @@ +{{ source }} clapped on {{target}}'s project "{{ project }}" diff --git a/zubhub_backend/zubhub/templates/activitylog/comment.html b/zubhub_backend/zubhub/templates/activitylog/comment.html new file mode 100644 index 000000000..168f1e3fd --- /dev/null +++ b/zubhub_backend/zubhub/templates/activitylog/comment.html @@ -0,0 +1 @@ +{{ source }} commented on {{target}}'s project "{{ project }}" diff --git a/zubhub_backend/zubhub/templates/activitylog/follow.html b/zubhub_backend/zubhub/templates/activitylog/follow.html new file mode 100644 index 000000000..ba38b6a17 --- /dev/null +++ b/zubhub_backend/zubhub/templates/activitylog/follow.html @@ -0,0 +1 @@ +{{ source }} started following {{target}} diff --git a/zubhub_backend/zubhub/zubhub/admin.py b/zubhub_backend/zubhub/zubhub/admin.py index 22dfda69e..22989b80c 100644 --- a/zubhub_backend/zubhub/zubhub/admin.py +++ b/zubhub_backend/zubhub/zubhub/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import AdminSettings, Hero, Privacy, FAQ, Help +from .models import AdminSettings, Hero, Privacy, FAQ, Help, Challenge, Ambassadors ## these models are imported to be unregsitered from django_summernote.models import Attachment @@ -28,7 +28,7 @@ class PrivacyAdmin(SummernoteModelAdmin): readonly_fields = ["edited_on"] class Media: - js = ('http://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) + js = ('https://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) class HelpAdmin(SummernoteModelAdmin): @@ -36,21 +36,38 @@ class HelpAdmin(SummernoteModelAdmin): readonly_fields = ["edited_on"] class Media: - js = ('http://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) + js = ('https://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) +class ChallengeAdmin(SummernoteModelAdmin): + summernote_fields = ('challenge',) + readonly_fields = ["edited_on"] + + class Media: + js = ('https://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) class FAQAdmin(SummernoteModelAdmin): summernote_fields = ('answer',) class Media: - js = ('http://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) + js = ('https://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) + + +class AmbassadorsAdmin(SummernoteModelAdmin): + summernote_fields = ('ambassadors',) + readonly_fields = ["edited_on"] + search_fields = ["projects"] + + class Media: + js = ('https://code.jquery.com/jquery-3.1.1.js', 'js/main.js',) admin.site.register(AdminSettings, AdminSettingsAdmin) admin.site.register(Hero, HeroAdmin) admin.site.register(Privacy, PrivacyAdmin) admin.site.register(Help, HelpAdmin) +admin.site.register(Challenge, ChallengeAdmin) admin.site.register(FAQ, FAQAdmin) +admin.site.register(Ambassadors, AmbassadorsAdmin) ## Unregister some default and third-party models admin.site.unregister(Attachment) diff --git a/zubhub_backend/zubhub/zubhub/migrations/0007_ambassadors.py b/zubhub_backend/zubhub/zubhub/migrations/0007_ambassadors.py new file mode 100644 index 000000000..067e40a4d --- /dev/null +++ b/zubhub_backend/zubhub/zubhub/migrations/0007_ambassadors.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2 on 2022-10-04 00:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zubhub', '0006_hero_tinkering_resource_url'), + ] + + operations = [ + migrations.CreateModel( + name='Ambassadors', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ambassadors', models.TextField(blank=True, null=True)), + ('edited_on', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Ambassadors', + 'verbose_name_plural': 'Ambassadors', + }, + ), + ] diff --git a/zubhub_backend/zubhub/zubhub/migrations/0008_ambassadors_projects.py b/zubhub_backend/zubhub/zubhub/migrations/0008_ambassadors_projects.py new file mode 100644 index 000000000..775c35feb --- /dev/null +++ b/zubhub_backend/zubhub/zubhub/migrations/0008_ambassadors_projects.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2022-11-07 04:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0007_tag_tag_name_gin_idx'), + ('zubhub', '0007_ambassadors'), + ] + + operations = [ + migrations.AddField( + model_name='ambassadors', + name='projects', + field=models.ManyToManyField(related_name='ambassador_project_picks', to='projects.Project'), + ), + ] diff --git a/zubhub_backend/zubhub/zubhub/migrations/0009_challenge.py b/zubhub_backend/zubhub/zubhub/migrations/0009_challenge.py new file mode 100644 index 000000000..04585fbf8 --- /dev/null +++ b/zubhub_backend/zubhub/zubhub/migrations/0009_challenge.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2 on 2022-12-10 22:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('zubhub', '0008_ambassadors_projects'), + ] + + operations = [ + migrations.CreateModel( + name='Challenge', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('challenge', models.TextField(blank=True, null=True)), + ('edited_on', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Challenge', + 'verbose_name_plural': 'Challenges', + }, + ), + ] diff --git a/zubhub_backend/zubhub/zubhub/models.py b/zubhub_backend/zubhub/zubhub/models.py index 4e99a00e5..5cde303de 100644 --- a/zubhub_backend/zubhub/zubhub/models.py +++ b/zubhub_backend/zubhub/zubhub/models.py @@ -6,6 +6,7 @@ from django.core.validators import FileExtensionValidator from django.utils.text import slugify from .utils import MediaStorage, get_upload_path, clean_summernote_html +from projects.models import Project class AdminSettings(models.Model): @@ -102,6 +103,21 @@ def save(self, *args, **kwargs): self.edited_on = timezone.now() super().save(*args, **kwargs) +class Challenge(models.Model): + challenge = models.TextField(blank=True, null=True) + edited_on = models.DateTimeField(blank=True, null=True) + + class Meta: + verbose_name = "Challenge" + verbose_name_plural = "Challenges" + + def __str__(self): + return self.edited_on.strftime("Challenges as edited on %I:%M %p, %d %b %Y %Z") + + def save(self, *args, **kwargs): + self.challenge = clean_summernote_html(self.challenge) + self.edited_on = timezone.now() + super().save(*args, **kwargs) class FAQ(models.Model): question = models.TextField(blank=True, null=True) @@ -118,3 +134,20 @@ def save(self, *args, **kwargs): self.question = clean_summernote_html(self.question) self.answer = clean_summernote_html(self.answer) super().save(*args, **kwargs) + + +class Ambassadors(models.Model): + ambassadors = models.TextField(blank=True, null=True) + edited_on = models.DateTimeField(blank=True, null=True) + projects = models.ManyToManyField(Project, related_name="ambassador_project_picks") + + class Meta: + verbose_name = "Ambassadors" + verbose_name_plural = "Ambassadors" + + def __str__(self): + return self.edited_on.strftime("Ambassadors as edited on %I:%M %p, %d %b %Y %Z") + + def save(self, *args, **kwargs): + self.edited_on = timezone.now() + super().save(*args, **kwargs) diff --git a/zubhub_backend/zubhub/zubhub/serializers.py b/zubhub_backend/zubhub/zubhub/serializers.py index 6923f5269..6743100c0 100644 --- a/zubhub_backend/zubhub/zubhub/serializers.py +++ b/zubhub_backend/zubhub/zubhub/serializers.py @@ -1,5 +1,10 @@ from rest_framework import serializers -from .models import Hero, FAQ, Help, Privacy +from .models import Hero, FAQ, Help, Challenge, Privacy, Ambassadors +from projects.pagination import ProjectNumberPagination +from projects.utils import get_published_projects_for_user +from projects.serializers import ProjectSerializer +from math import ceil + class HeroSerializer(serializers.ModelSerializer): @@ -39,6 +44,13 @@ class Meta: "about", ] +class ChallengeSerializer(serializers.ModelSerializer): + + class Meta: + model = Challenge + fields = [ + "challenge", + ] class FAQListSerializer(serializers.ModelSerializer): @@ -48,3 +60,42 @@ class Meta: "question", "answer" ] + + +class AmbassadorsSerializer(serializers.ModelSerializer): + projects = serializers.SerializerMethodField('paginated_projects') + + class Meta: + model = Ambassadors + fields = [ + "ambassadors", + "projects" + ] + + def paginated_projects(self, obj): + projects = obj.projects.all() + + projects = get_published_projects_for_user( + self.context['request'].user, + projects) + + paginator = ProjectNumberPagination() + page = paginator.paginate_queryset( + projects, self.context['request']) + serializer = ProjectSerializer(page, read_only=True, many=True, context={ + 'request': self.context['request']}) + count = projects.count() + num_pages = ceil(count/paginator.page_size) + current_page = int( + self.context["request"].query_params.get("page", "1")) + if current_page != 1: + prev_page = current_page - 1 + else: + prev_page = None + + if current_page != num_pages: + next_page = current_page + 1 + else: + next_page = None + + return {"results": serializer.data, "prev": prev_page, "next": next_page, "count": count} \ No newline at end of file diff --git a/zubhub_backend/zubhub/zubhub/settings.py b/zubhub_backend/zubhub/zubhub/settings.py index fb8b87243..1139d07ad 100644 --- a/zubhub_backend/zubhub/zubhub/settings.py +++ b/zubhub_backend/zubhub/zubhub/settings.py @@ -113,7 +113,9 @@ 'APIS', 'creators', 'projects', - 'notifications' + 'notifications', + 'activitylog', + 'activities', ] # askimet @@ -334,7 +336,8 @@ ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], - ['insert', ['emoji', 'link', 'picture', 'video']] + ['insert', ['emoji', 'link', 'picture', 'video']], + ['view', ['codeview']] ] }, "css": ("/static/css/summernote_plugin.css",), diff --git a/zubhub_backend/zubhub/zubhub/urls.py b/zubhub_backend/zubhub/zubhub/urls.py index 448655c04..09788d377 100644 --- a/zubhub_backend/zubhub/zubhub/urls.py +++ b/zubhub_backend/zubhub/zubhub/urls.py @@ -30,10 +30,6 @@ ] - - - - if settings.DEBUG: import debug_toolbar urlpatterns = [ @@ -44,11 +40,12 @@ if settings.DEFAULT_BACKEND_DOMAIN.startswith("localhost"): """ Don't server documentation and schema in production """ - from rest_auth.urls import (LoginView, LogoutView, PasswordResetConfirmView, PasswordResetView) + from rest_auth.urls import ( + LoginView, LogoutView, PasswordResetConfirmView, PasswordResetView) from rest_auth.registration.urls import VerifyEmailView from zubhub.views import (UploadFileAPIView, DeleteFileAPIView, - HeroAPIView, HelpAPIView, PrivacyAPIView, - FAQAPIView, SigGenAPIView, UploadFileToLocalAPIView, + HeroAPIView, HelpAPIView, ChallengeAPIView, PrivacyAPIView, + FAQAPIView, AmbassadorsAPIView, SigGenAPIView, UploadFileToLocalAPIView, MarkdownToHtmlAPIView, MediaSchemaAPIView, WebSchemaAPIView) schema_url_patterns = [ @@ -59,13 +56,17 @@ path('api/rest-auth/password/reset/confirm/', PasswordResetConfirmView.as_view()), path('api/creators/', include('creators.urls')), path('api/projects/', include('projects.urls')), + path('api/activities/', include('activities.urls')), + path('api/activitylog/', include('activitylog.urls')), path('api/upload-file/', UploadFileAPIView), path('api/delete-file/', DeleteFileAPIView), path('api/upload-file-to-local/', UploadFileToLocalAPIView), path('api/hero/', HeroAPIView.as_view()), path('api/help/', HelpAPIView.as_view()), + path('api/challenge/', ChallengeAPIView.as_view()), path('api/privacy/', PrivacyAPIView.as_view()), path('api/faqs/', FAQAPIView.as_view()), + path('api/ambassadors/', AmbassadorsAPIView.as_view()), path('api/signature/', SigGenAPIView) ] @@ -74,6 +75,6 @@ path('media-schema/', MediaSchemaAPIView, name="media-schema"), path('', TemplateView.as_view( template_name='doc/doc.html', - extra_context={'web_schema_url':'web-schema', 'media_schema_url': 'media-schema'}), name="doc"), + extra_context={'web_schema_url': 'web-schema', 'media_schema_url': 'media-schema'}), name="doc"), path('markdown-to-html/', MarkdownToHtmlAPIView, name="markdown-to-html") ] diff --git a/zubhub_backend/zubhub/zubhub/views.py b/zubhub_backend/zubhub/zubhub/views.py index 3108477f5..7805abe52 100644 --- a/zubhub_backend/zubhub/zubhub/views.py +++ b/zubhub_backend/zubhub/zubhub/views.py @@ -1,7 +1,7 @@ from math import floor import uuid -from .serializers import HeroSerializer, FAQListSerializer, HelpSerializer, PrivacySerializer -from .models import Hero, FAQ, Privacy, Help, AdminSettings +from .serializers import HeroSerializer, FAQListSerializer, HelpSerializer, ChallengeSerializer, PrivacySerializer, AmbassadorsSerializer +from .models import Hero, FAQ, Privacy, Help, Challenge, Ambassadors, AdminSettings from .utils import delete_file_from_media_server, upload_file_to_media_server, get_sig from projects.permissions import PostUserRateThrottle, GetUserRateThrottle, SustainedRateThrottle from rest_framework.permissions import IsAuthenticated @@ -209,6 +209,18 @@ class HelpAPIView(RetrieveAPIView): def get_object(self): return self.get_queryset().last() +class ChallengeAPIView(RetrieveAPIView): + """ + Get "Challenges"\n + """ + + queryset = Challenge.objects.all() + serializer_class = ChallengeSerializer + permission_classes = [AllowAny] + throttle_classes = [GetUserRateThrottle, SustainedRateThrottle] + + def get_object(self): + return self.get_queryset().last() class PrivacyAPIView(RetrieveAPIView): """ @@ -235,6 +247,19 @@ class FAQAPIView(ListAPIView): throttle_classes = [GetUserRateThrottle, SustainedRateThrottle] +class AmbassadorsAPIView(RetrieveAPIView): + """ + Get "Ambassadors"\n + """ + + queryset = Ambassadors.objects.all() + serializer_class = AmbassadorsSerializer + permission_classes = [AllowAny] + throttle_classes = [GetUserRateThrottle, SustainedRateThrottle] + + def get_object(self): + return self.get_queryset().last() + @api_view(['GET']) @permission_classes([AllowAny]) diff --git a/zubhub_frontend/zubhub/.env.example b/zubhub_frontend/zubhub/.env.example index 51371c437..8c7d4413a 100644 --- a/zubhub_frontend/zubhub/.env.example +++ b/zubhub_frontend/zubhub/.env.example @@ -7,4 +7,5 @@ REACT_APP_VIDEO_UPLOAD_URL= REACT_APP_VIDEO_FOLDER_NAME= REACT_APP_DEV_VIDEO_FOLDER_NAME= REACT_APP_VIDEO_UPLOAD_PRESET_NAME=