diff --git a/accounts/forms.py b/accounts/forms.py index f9413978..bfe17b77 100755 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -36,11 +36,7 @@ class Meta: 'stream', 'gender', 'position', - 'bio' ) # Note that we didn't mention user field here. - widgets = { - 'bio': forms.Textarea(attrs={'cols': 80, 'rows': 20}), - } def save(self, user=None): user_profile = super(UserProfileForm, self).save(commit=False) diff --git a/accounts/models.py b/accounts/models.py index 33985e61..fcc3c224 100755 --- a/accounts/models.py +++ b/accounts/models.py @@ -15,15 +15,54 @@ def __str__(self): class UserProfile(models.Model): + branch_choices = ( + ('CS', 'CSE'), + ('IT', 'IT'), + ('CC', 'CCE'), + ('ME', 'MECHANICAL'), + ('CV', 'CIVIL'), + ('EC', 'ECE'), + ('EE', 'EE'), + ('CM', 'CHEMICAL') + ) + + year_choices = ( + (1, 'One'), + (2, 'Two'), + (3, 'Three'), + (4, 'Four'), + ) + + stream_choices = ( + ('BT', 'B.Tech'), + ('BH', 'B.Hons'), + ('BJ', 'BJMC'), + ('BS', 'BSc'), + ('BC', 'BCA') + ) + + gender_choices = ( + ('M', 'Male'), + ('F', 'Female'), + ('O', 'Other') + ) + + position_choices = ( + ('ST', 'Student'), + ('PR', 'Professor'), + ('TA', 'Teaching Assistant'), + ('CO', 'Company') + ) + user = models.OneToOneField(User, on_delete=models.CASCADE, null=False) ratings = models.IntegerField(null=True, default=0, blank=True) photo = models.ImageField(upload_to="profile_image", null=True, blank=True) - year = models.IntegerField(null=True, default=1, blank=True) - branch = models.CharField(max_length=20, default="Not Updated", blank=True, null=True) - stream = models.CharField(max_length=20, default="Not Updated", blank=True, null=True) - gender = models.CharField(max_length=20, default="Not Updated", blank=True, null=True) - position = models.CharField(max_length=20, default="Not Updated", blank=True, null=True) # Student or Teacher - bio = models.TextField() + year = models.IntegerField(null=True, default=1, blank=True, choices=year_choices) + branch = models.CharField(max_length=20, default="Not Updated", blank=True, null=True, choices=branch_choices) + stream = models.CharField(max_length=20, default="Not Updated", blank=True, null=True, choices=stream_choices) + gender = models.CharField(max_length=20, default="Not Updated", blank=True, null=True, choices=gender_choices) + position = models.CharField(max_length=20, default="Not Updated", blank=True, null=True, choices=position_choices) # Student or Teacher + bio = models.TextField(help_text="Add some information about yourself") follows = models.ManyToManyField('self', related_name='followers', symmetrical=False, blank=True) class Meta: diff --git a/accounts/static/accounts/css/style.css b/accounts/static/accounts/css/style.css index ef585ca1..58416940 100644 --- a/accounts/static/accounts/css/style.css +++ b/accounts/static/accounts/css/style.css @@ -4,10 +4,7 @@ body { -webkit-font-smoothing: antialiased; background: #E4E4E4; } -.first{ -/* background-color: #bab9bf;*/ -} .img-thumbnail{ height: 180px; width: 180px; @@ -29,9 +26,90 @@ body { .third{ margin-left: 100px; } -/*.border{ - border-color: #0094ff; -}*/ + .move{ margin-left: 35px; +} + +.hidden-form { + display: none; +} + +#skill-form { + padding: 10px 0 10px 0; +} + +#skill { + height: 70px; +} + +#profile-pic-save { + margin-top: 3.2em; + display: none; +} + +.delete-skill:hover { + cursor: pointer; +} + +#update-info-now { + display: none; + width: 90%; + height: 75%; + position: fixed; + z-index: 1; + left: 0; + top: 0; + overflow: auto; + background-color: #ffffff; + box-shadow: 1px 2px 7px 2px#111111; + margin: 20vh 0 0 5%; + padding: 20px; +} + +#modal-title { + height: 20%; + display: grid; + grid-template-columns: repeat(5, 1fr); +} + +#modal-title-1 { + grid-row: 1; + grid-column: 1 / 5; +} + +.grided { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-gap: 20px; +} + +#update-submit-button { + margin: auto; + grid-column: 5; + grid-row: 2; +} + +.skill-row { + display: flex; + flex-direction: row; +} + +.skill-title { + width: 80%; +} + +.skill-delete-form { + width: 20%; +} + +#spinner-profile-pic { + display: none; + width: 10em; +} + +#save-pic-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 20px; } \ No newline at end of file diff --git a/accounts/static/accounts/img/spinner.gif b/accounts/static/accounts/img/spinner.gif new file mode 100644 index 00000000..1b1015a0 Binary files /dev/null and b/accounts/static/accounts/img/spinner.gif differ diff --git a/accounts/static/accounts/js/ajaxWrapper.js b/accounts/static/accounts/js/ajaxWrapper.js new file mode 100644 index 00000000..7bcf9d04 --- /dev/null +++ b/accounts/static/accounts/js/ajaxWrapper.js @@ -0,0 +1,155 @@ +/* +MIT License + +Copyright (c) 2020 daniel muremwa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Visit: https://github.com/muremwa/read-urls-extension.git + +*/ + +const ajax = (() => { + const requestType = { + POST: 'POST', + GET: 'GET' + }; + const crossSiteHeader = { + name: 'Access-Control-Allow-Origin', + value: '*' + }; + const flat200 = (status) => { + return parseInt(status.toString().replace(/\B\d/g, '0')); + }; + const xhr = new XMLHttpRequest(); + const addHeaders = (headers, cross) => { + cross ? headers.push(crossSiteHeader) : void 0; + headers.forEach((header) => { + xhr.setRequestHeader(header.name, header.value); + }); + }; + function __superRequest__(options) { + // cookies? + options.sendCookies === true ? xhr.withCredentials = true : void 0; + // add headers if any + options.headers && options.headers.length > 0 ? addHeaders(options.headers, options.crosssite) : void 0; + // set response type + xhr.responseType = options.responseType; + // error handler + xhr.onerror = options.error; + // request complete + xhr.onload = (event) => { + if (flat200(xhr.status) === 200) { + options.success({ + status: xhr.status, + statusText: xhr.statusText, + response: xhr.response + }); + } + else { + options.error(event); + } + ; + }; + } + ; + /* + send a get request + */ + function _get_request_(options) { + // add search params if any + if (options.params) { + if (typeof options.url === 'string') { + options.url = new URL(options.url); + } + ; + if (!('searchParams' in options.url)) { + throw TypeError('The url passed is incorrect'); + } + ; + options.params.forEach((param) => { + options.url.searchParams.set(param.name, param.value); + }); + } + ; + xhr.open(requestType.GET, options.url); + // download progress + if (options.downloadprogress) { + xhr.onprogress = (event) => { + if (event.lengthComputable) { + options.downloadprogress(event.lengthComputable, event.loaded, event.total); + } + else { + options.downloadprogress(event.lengthComputable, event.loaded); + } + ; + }; + } + ; + // call generic options + __superRequest__(options); + // send request bro + xhr.send(); + } + ; + /* + send A post request + */ + function _post_request_(options) { + if (options.data && options.form) { + throw new Error('Both data and form are currently not supported'); + } + ; + // open the request + xhr.open(requestType.POST, options.url); + // data to be sent to back end? + let _data = ''; + if (options.data) { + const jsonHeader = { + name: 'Content-type', + value: 'application/json; charset=utf-8' + }; + options.headers ? options.headers.push(jsonHeader) : options.headers = [jsonHeader,]; + _data = JSON.stringify(options.data); + } + else if (options.form) { + _data = new FormData(options.form); + } + ; + // call generic options + __superRequest__(options); + // upload progress start? + options.uploadstart ? xhr.upload.onloadstart = options.uploadstart : void 0; + // upload done? + options.uploadend ? xhr.upload.onload = options.uploadend : void 0; + // upload progress error? + options.uploaderror ? xhr.upload.onerror = options.uploaderror : void 0; + // upload progress? + options.uploadprogress ? xhr.upload.onprogress = (event) => { + options.uploadprogress(event.loaded, event.total); + } : void 0; + // send request + xhr.send(_data); + } + ; + return { + get: (options) => _get_request_(options), + post: (options) => _post_request_(options), + }; +})(); \ No newline at end of file diff --git a/accounts/static/accounts/js/profile.js b/accounts/static/accounts/js/profile.js new file mode 100644 index 00000000..7e39d0f7 --- /dev/null +++ b/accounts/static/accounts/js/profile.js @@ -0,0 +1,303 @@ +/* + This function toggles the display status of a form + pass it a button that's used to toogle + the button should have 2 attributes + 1. 'data-form-id': id of the form to toogle display. + 2. 'data-og-text': the original text on the button. +*/ +function toogleForm (target) { + const formId = target.dataset.formId; + const form = document.getElementById(formId); + + if (form) { + if (form.style.display === 'none' || !form.style.display) { + form.style.display = 'block'; + target.innerText = 'close form'; + } else { + form.style.display = 'none'; + target.innerText = target.dataset.ogText; + }; + }; +}; + + +// add listener to buttons to show forms +[...document.getElementsByClassName('toggle-form')].forEach((button) => { + button.addEventListener('click', (event) => { + toogleForm(event.target); + }); +}); + + +/* + Profile pic upload +*/ +function uploadPic () { + document.getElementById('profile-pic').click(); +}; + +const previewWarning = document.getElementById('preview-warning'); + +document.getElementById('profile-pic').addEventListener('change', (event) => { + // preview new profile pic for the user + const reader = new FileReader(); + reader.onload = (event) => document.getElementById('user-profile-pic').src = event.target.result; + reader.readAsDataURL(event.target.files[0]); + + // choose another picture + document.getElementById('profile-pic-upload').innerText = 'choose another picture'; + // save button + document.getElementById('profile-pic-save').style.display = 'block'; + previewWarning.style.display = 'block'; +}); + + +const savePictureButton = document.getElementById('profile-pic-save'); + +savePictureButton.addEventListener('click', () => { + const picForm = document.forms['profile-pic-form']; + const spinnerImg = document.getElementById('spinner-profile-pic'); + spinnerImg.src = spinnerImg.dataset.src; + spinnerImg.style.display = 'block'; + + const profilePicOptions = { + url: picForm.action, + responseType: 'json', + error: () => { + document.getElementById('pic-save-error').style.display = ''; + previewWarning.style.display = 'none'; + spinnerImg.style.display = 'none'; + }, + success: () => { + previewWarning.style.display = 'none'; + savePictureButton.style.display = 'none'; + spinnerImg.style.display = 'none'; + }, + form: picForm + }; + + ajax.post(profilePicOptions); +}); + + +/* + update info now +*/ + +// open update-form +document.getElementById('update-info-btn').addEventListener('click', () => { + document.getElementById('update-info-now').style.display = 'block'; +}) + + +// send form details +const modalCloseButton = document.getElementById('modal-close-button'); +const infoForm = document.forms['update-info-form']; +const submitInfoButton = document.getElementById('info-submit-button'); +const modalCloseCallback = (event) => { + submitInfoButton.disabled = true; + document.getElementById(event.target.dataset.modalId).style.display = 'none'; +}; + +if (infoForm) { + infoForm.addEventListener('change', () => submitInfoButton.disabled = false); + + infoForm.addEventListener('submit', (event) => { + event.preventDefault(); + modalCloseButton.disabled = true; + const title = document.getElementById('modal-title-1'); + title.innerHTML = '

Updating info...

'; + submitInfoButton.disabled = true; + + const infoOptions = { + url: infoForm.action, + responseType: 'json', + error: () => { + document.getElementById('info-update-error').style.display = 'block'; + submitInfoButton.disabled = false; + }, + success: (result) => { + const response = result.response; + if (response.success) { + document.getElementById('info-stream').innerText = `Stream : ${response.profile.stream}`; + document.getElementById('info-branch').innerText = `Branch : ${response.profile.branch}`; + document.getElementById('info-year').innerText = `Year : ${response.profile.year}`; + title.innerHTML = '

Update info

'; + modalCloseButton.disabled = false; + modalCloseCallback(event); + }; + }, + form: infoForm + }; + + ajax.post(infoOptions); + }); +}; + + +// close update form +modalCloseButton.addEventListener('click', modalCloseCallback); + + + +/* + Bio submition +*/ +const bioForm = document.forms['bio-form']; + +if (bioForm) { + const submitInfoButton = document.getElementById('bio-form-sub'); + bioForm.onchange = () => submitInfoButton.disabled = false; + + bioForm.onsubmit = (event) => { + event.preventDefault(); + submitInfoButton.innerText = 'Updating...'; + + const options = { + url: bioForm.action, + responseType: 'json', + error: () => { + submitInfoButton.innerText = 'Could not update!'; + document.getElementById('bio-error').style.display = 'block'; + bioForm[1].disabled = true; + }, + success: () => { + document.getElementById('bio-div').innerText = bioForm[1].value; + submitInfoButton.innerText = 'Update bio' + toogleForm(document.getElementById('bio-form-toogle')); + }, + form: bioForm + }; + + ajax.post(options); + }; + +}; + + +/* + Delete skill +*/ + +const skillDeleteCallback = (skillDeleteButton) => { + // delete skill + const skillDeleteOptions = { + url: skillDeleteButton.parentElement.action, + responseType: 'json', + error: () => { + document.getElementById(skillDeleteButton.dataset.errorId).style.display = 'block'; + }, + success: () => { + document.getElementById(skillDeleteButton.dataset.formId).innerText = 'Skill deleted!' + }, + form: skillDeleteButton.parentElement, + headers: [ + {name: 'X-Requested-With', value: 'XMLHttpRequest'}, + ] + }; + + ajax.post(skillDeleteOptions); +}; + + +[...document.getElementsByClassName('delete-skill')].forEach((skillDeleteButton) => { + skillDeleteButton.addEventListener('click', () => { + skillDeleteCallback(skillDeleteButton); + }); +}); + + +/* + add skill +*/ +const skillForm = document.forms['skill-form']; + +if (skillForm) { + const submitSkillButton = [...skillForm.elements].find((element) => element.type === 'submit'); + skillForm.addEventListener('change', () => { + submitSkillButton.disabled = false; + }); + + skillForm.addEventListener('submit', (event) => { + event.preventDefault(); + + const skillAddOptions = { + url: skillForm.action, + responseType: 'json', + error: () => { + if (submitSkillButton) { + submitSkillButton.value = 'Could not add skill!'; + submitSkillButton.disabled = true; + } + document.getElementById('skill-error').style.display = 'block'; + skillForm[1].disabled = true; + }, + success: (result) => { + const response = result.response; + if (response.success) { + const token = skillForm.elements['csrfmiddlewaretoken']? skillForm.elements.csrfmiddlewaretoken.value: ''; + // add correct details to the function call below + createSkill(response.skill.name, response.skill.id, token, response.skill.delete_skill_url); + const noSkills = document.getElementById('no-skills-div'); + if (noSkills) { + noSkills.style.display = 'none'; + }; + toogleForm(document.getElementById('add-skill-toogle')); + }; + }, + form: skillForm, + headers: [ + {name: 'X-Requested-With', value: 'XMLHttpRequest'}, + ] + }; + + ajax.post(skillAddOptions); + }); +}; + + +/* + create a new skill div +*/ +function createSkill(skillText, skillId, formToken, formAction) { + const topDiv = document.createElement('div'); + topDiv.id = `skill-${skillId}`; + + const deleteError = document.createElement('span'); + deleteError.id = `skill-${skillId}-del-error`; + deleteError.style.display = 'none'; + deleteError.innerText = 'Could Delete skill, please refresh the page and try again'; + topDiv.appendChild(deleteError); + + const skillRow = document.createElement('div'); + skillRow.className = "skill-row"; + + const skillTitle = document.createElement('span'); + skillTitle.innerText = skillText; + skillTitle.className = "skill-title"; + skillRow.appendChild(skillTitle); + + + const deleteForm = document.createElement('form'); + deleteForm.action = formAction; + deleteForm.className = "skill-delete-form"; + deleteForm.method = 'POST'; + [['csrfmiddlewaretoken', formToken], ['skill-id', skillId]].forEach((input) => { + const _input = document.createElement('input'); + _input.type = 'hidden'; + _input.name = input[0]; + _input.value = input[1]; + deleteForm.appendChild(_input); + }); + const submitSpan = document.createElement('span'); + submitSpan.classList = 'fa fa-trash delete-skill'; + submitSpan.dataset.formId = topDiv.id; + submitSpan.dataset.errorId = deleteError.id; + deleteForm.appendChild(submitSpan); + submitSpan.addEventListener('click', () => skillDeleteCallback(submitSpan)); + skillRow.appendChild(deleteForm); + + topDiv.appendChild(skillRow); + topDiv.appendChild(document.createElement('hr')); + document.getElementById('all-skills').appendChild(topDiv); +}; \ No newline at end of file diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html index ee5dd587..66194bdb 100755 --- a/accounts/templates/accounts/profile.html +++ b/accounts/templates/accounts/profile.html @@ -27,12 +27,15 @@

Ms. {{ user.first_name }} {{ user.last_name }}

{{ user.first_name }} {{ user.last_name }}

{% endif %} {% if user.userprofile.photo %} - user image {% else %} - user image {% endif %} +
+ + @@ -45,25 +48,26 @@
- - - @@ -78,17 +82,17 @@
Following : {{ following }}
@@ -114,6 +118,49 @@
- Upload Pic +
+ {% csrf_token %} + +
+
+
+ +
+ +
+
- Update Info -
-
-
- Add - Skill -
+ + +
-
Stream : {{ user.userprofile.stream }}
+
Stream : {{ user.userprofile.stream }}
-
Branch : {{ user.userprofile.branch }}
+
Branch : {{ user.userprofile.branch }}
-
Year : {{ user.userprofile.year }}
+
Year : {{ user.userprofile.year }}
+
+ + +
+ {% csrf_token %} +
+
+ +
+ {{ profile_form.year }} +
+
+ +
+ {{ profile_form.branch }} +
+
+ +
+ {{ profile_form.stream }} +
+
+ +
+ {{ profile_form.gender }} +
+
+ +
+ {{ profile_form.position }} +
+ +
+ +
+
+
+
@@ -122,34 +169,68 @@

BIO


-
{% if user.userprofile.bio|length == 0 %} No bio - yet! {% else %} - {{ user.userprofile.bio|linebreaks }} {% endif %}
+
+ {% if user.userprofile.bio|length == 0 %} +
No bio yet!
+ {% else %} +
{{ user.userprofile.bio|linebreaks }}
+ {% endif %} + +
+
+ {% csrf_token %} + +
+ +
+
+ +
+

- +

-
-
-

Skills

+
+
+
+

Skills

+
+
+ +
+ {% if skills %} + {% for skill in skills %} +
+ + +
+ {{ skill }} +
+ {% csrf_token %} + + +
+
+
+
+ {% endfor %} + {% else %} +

You haven't added any skills to your profile!

+ {% endif %}
-
- - - - - {% for skill in skills %} - - - - - - {% empty %} - - - {% endfor %} - -
{{ skill }}
You haven't added any skills to your profile!
+ +
+ {% csrf_token %} + +
+ +
+
+ +
+
+

Interested in

@@ -185,3 +266,9 @@

Interested in

{% endblock %} + + +{% block customjs %} + + +{% endblock customjs %} \ No newline at end of file diff --git a/accounts/urls.py b/accounts/urls.py index 937411ec..36b02c5b 100755 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -24,4 +24,7 @@ path('profile/addskill/', views.add_skill_view, name='addskill'), path('profile/delete_skill/', views.delete_skill, name='deleteskill'), path("chat/", include("nspmessage.urls")), + path('profile/update-pic/', views.update_profile_picture, name='update-picture'), + path('profile/update-bio/', views.update_bio, name='update-bio'), + path('profile/update-profile-info/', views.update_user_profile_info, name='update-profile'), ] diff --git a/accounts/views.py b/accounts/views.py index 29f58588..ec76de3c 100755 --- a/accounts/views.py +++ b/accounts/views.py @@ -46,7 +46,7 @@ def profile_view(request): , "followings": followings, "skills": skills} rating_value = user.userprofile.ratings args = {'user': user, "followers": followers, "following": followings, "skills": skills, - 'range': range(rating_value), 'projects': projects} + 'range': range(rating_value), 'projects': projects, 'profile_form': UserProfileForm(instance=user.userprofile)} return render(request, 'accounts/profile.html', args) @@ -187,6 +187,10 @@ def delete_skill(request, ID): skill = get_object_or_404(Skill,pk=ID) if skill.user == request.user: skill.delete() + + if request.is_ajax(): + return JsonResponse({'success': True}) + return redirect("/account/profile/") @@ -198,7 +202,19 @@ def add_skill_view(request): form = SkillForm() skill = request.POST.get("skill") skill_object = Skill.objects.create(user=request.user, skill_name=skill) - return render(request, 'accounts/addskill.html', {'form': form, "successfully": True, "skill": skill_object}) + + if request.is_ajax(): + res_skill = { + 'name': skill, + 'id': skill_object.pk, + 'delete_skill_url': reverse("deleteskill", kwargs={"ID": str(skill_object.pk)}) + } + return JsonResponse({ + 'success': True, + 'skill': res_skill + }) + else: + return render(request, 'accounts/addskill.html', {'form': form, "successfully": True, "skill": skill_object}) else: form = SkillForm() return render(request, 'accounts/addskill.html', {'form': form}) @@ -254,3 +270,42 @@ def get_object(self, *args, **kwargs): def get_success_url(self, *args, **kwargs): return reverse("view_profile") + + +def update_profile_picture(request): + profile_picture = request.FILES.get('profile-picture') + request.user.userprofile.photo = profile_picture + request.user.userprofile.save() + return JsonResponse({'success': True}) + + +def update_bio(request): + new_bio = request.POST.get('user-bio') + request.user.userprofile.bio = new_bio + request.user.userprofile.save() + return JsonResponse({'success': True}) + + +def update_user_profile_info(request): + if request.method == 'POST': + form = UserProfileForm(request.POST, instance=request.user.userprofile) + response = { + 'success': False + } + + if form.is_valid(): + form.save() + response.update({ + 'success': True, + 'profile': { + 'stream': request.user.userprofile.stream, + 'branch': request.user.userprofile.branch, + 'year': request.user.userprofile.year + } + }) + else: + response.update({ + 'errors': form.errors + }) + + return JsonResponse(response) diff --git a/nsp/templates/base.html b/nsp/templates/base.html index ce6d3853..d4da6a49 100755 --- a/nsp/templates/base.html +++ b/nsp/templates/base.html @@ -91,6 +91,7 @@ + {% block head %}{% endblock head %} {% block sadana %} {% endblock %} + {% block customjs %}{% endblock customjs %}