Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Account and Profile related code to accounts app #926

Closed
14 tasks
brylie opened this issue Jun 14, 2021 · 18 comments · Fixed by #1011
Closed
14 tasks

Move Account and Profile related code to accounts app #926

brylie opened this issue Jun 14, 2021 · 18 comments · Fixed by #1011
Assignees
Milestone

Comments

@brylie
Copy link
Member

brylie commented Jun 14, 2021

We still have some code in the api directory that is related to user accounts/profiles. Move all account/profile code to the accounts app and refactor as needed.

Relatedly, rename the Account model to Profile.

Task

Below are the overarching steps involved in completing this issue. Some steps are optional but might help us achieve cleaner and more maintainable code.

  • Move all code indicated below to the accounts app
  • refactor related code to reference the new location
  • (optional) remove redundant code, if possible
  • (optional) clean-up existing code if there are more conventional ways of doing things

Related code

All code below should be moved to the accounts app.

  • Move Account model to the accounts.models and rename the model to Profile:

    """
    Account Model
    Extends the default django user model

  • Move Accounts views to the accounts app:

  • https://github.com/CiviWiki/OpenCiviWiki/blob/develop/project/api/views/account.py

  • Move account/password related forms to accounts app:

  • https://github.com/CiviWiki/OpenCiviWiki/blob/develop/project/api/forms.py

  • Move User serializer to accounts app

    User = get_user_model()
    def get_user(request, user):
    """
    USAGE:
    This is used to get a user
    """
    try:
    u = User.objects.get(username=user)
    a = Account.objects.get(user=u)
    return JsonResponse(model_to_dict(a))
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))

  • User card serializer

    def get_card(request, user):
    """
    USAGE:
    This is used to get a card
    """
    try:
    u = User.objects.get(username=user)
    a = Account.objects.get(user=u)
    result = Account.objects.card_summarize(
    a, Account.objects.get(user=request.user)
    )
    return JsonResponse(result)
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))
    except Exception as e:
    return HttpResponseBadRequest(reason=str(e))

  • Profile serializer

  • def get_profile(request, user):
    """
    USAGE:
    This is used to get a user profile
    """
    try:
    u = User.objects.get(username=user)
    a = Account.objects.get(user=u)
    result = Account.objects.summarize(a)
    result["issues"] = []
    voted_solutions = Activity.objects.filter(
    account=a.id, civi__c_type="solution", activity_type__contains="pos"
    )
    solution_threads = voted_solutions.distinct("thread__id").values_list(
    "thread__id", flat=True
    )
    for thread_id in solution_threads:
    t = Thread.objects.get(id=thread_id)
    solutions = []
    solution_civis = voted_solutions.filter(thread=thread_id).values_list(
    "civi__id", flat=True
    )
    for civi_id in solution_civis:
    c = Civi.objects.get(id=civi_id)
    vote = voted_solutions.get(civi__id=civi_id).activity_type
    vote_types = {"vote_pos": "Agree", "vote_vpos": "Strongly Agree"}
    solution_item = {
    "id": c.id,
    "title": c.title,
    "body": c.body,
    "user_vote": vote_types.get(vote),
    }
    solutions.append(solution_item)
    my_issue_item = {
    "thread_id": t.id,
    "thread_title": t.title,
    "category": t.category.name,
    "solutions": solutions,
    }
    result["issues"].append(my_issue_item)
    if request.user.username != user:
    ra = Account.objects.get(user=request.user)
    if user in ra.following.all():
    result["follow_state"] = True
    else:
    result["follow_state"] = False
    return JsonResponse(result)
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))

  • Account-related read-only serializers

    class AccountCommonSerializer(serializers.ModelSerializer):
    """ Common serializer for specific account serializers"""
    username = serializers.ReadOnlyField(source="user.username")
    is_following = serializers.SerializerMethodField()
    def get_is_following(self, obj):
    request = self.context.get("request")
    # Check for authenticated user
    if request and hasattr(request, "user") and request.user.is_authenticated:
    account = Account.objects.get(user=request.user)
    if obj in account.following.all():
    return True
    return False
    class AccountSerializer(AccountCommonSerializer):
    """
    General seralizer for a single model instance of a user account
    """
    email = serializers.ReadOnlyField(source="user.email")
    profile_image = serializers.ImageField(
    write_only=True, allow_empty_file=False, required=False
    )
    profile_image_url = serializers.ReadOnlyField()
    profile_image_thumb_url = serializers.ReadOnlyField()
    address = serializers.CharField(allow_blank=True)
    zip_code = serializers.CharField(allow_blank=True)
    longitude = serializers.FloatField(max_value=180, min_value=-180, required=False)
    latitude = serializers.FloatField(max_value=90, min_value=-90, required=False)
    location = serializers.ReadOnlyField()
    is_staff = serializers.ReadOnlyField(source="user.is_staff")
    class Meta:
    model = Account
    fields = (
    "username",
    "first_name",
    "last_name",
    "about_me",
    "location",
    "email",
    "address",
    "city",
    "state",
    "zip_code",
    "country",
    "longitude",
    "latitude",
    "profile_image",
    "profile_image_url",
    "profile_image_thumb_url",
    "is_staff",
    "is_following",
    )
    extra_kwargs = {
    "city": WRITE_ONLY,
    "state": WRITE_ONLY,
    "country": WRITE_ONLY,
    }
    def validate_profile_image(self, value):
    """This function is used to validate the profile image before added to the user profile"""
    request = self.context["request"]
    validation_form = UpdateProfileImage(request.POST, request.FILES)
    if validation_form.is_valid():
    # Clean up previous images
    account = Account.objects.get(user=request.user)
    account.profile_image.delete()
    account.profile_image_thumb.delete()
    return validation_form.clean_profile_image()
    else:
    raise serializers.ValidationError(validation_form.errors["profile_image"])
    class AccountListSerializer(AccountCommonSerializer):
    """
    Serializer for multiple account model instances
    """
    first_name = serializers.ReadOnlyField()
    last_name = serializers.ReadOnlyField()
    location = serializers.ReadOnlyField()
    profile_image_url = serializers.ReadOnlyField()
    profile_image_thumb_url = serializers.ReadOnlyField()
    class Meta:
    model = Account
    fields = (
    "username",
    "first_name",
    "last_name",
    "profile_image_url",
    "profile_image_thumb_url",
    "location",
    "is_following",
    )

  • Account and profile related URLs

    router.register(r"accounts", views.AccountViewSet)

    url(r"^account_data/(?P<user>[-\w]+)/$", read.get_user, name="get user"),
    url(r"^account_profile/(?P<user>[-\w]+)/$", read.get_profile, name="get profile"),
    url(r"^account_card/(?P<user>[-\w]+)$", read.get_card, name="get card"),

    url(r"^edituser/$", write.editUser, name="edit user"),
    url(r"^upload_profile/$", write.uploadProfileImage, name="upload profile"),
    url(r"^upload_images/$", write.uploadCiviImage, name="upload images"),
    url(r"^upload_image/$", write.uploadThreadImage, name="upload image"),
    url(r"^clear_profile/$", write.clearProfileImage, name="clear profile"),
    url(r"^follow/$", write.requestFollow, name="follow user"),
    url(r"^unfollow/$", write.requestUnfollow, name="unfollow user"),
    url(
    r"^edit_user_categories/$",
    write.editUserCategories,
    name="edit user categories",
    ),

  • user-related utility function

    def get_account(user=None, pk=None, username=None):
    """ gets author based on the user """
    if user:
    return get_object_or_404(Account, user=user)
    elif pk:
    return get_object_or_404(Account, pk=pk)
    elif username:
    return get_object_or_404(Account, user__username=username)
    else:
    raise Http404

  • user/profile "write" endpoints

    @login_required
    def uploadphoto(request):
    """
    This function is a work in progress
    Eventually will be used to allow users to upload photos
    """
    pass
    @login_required
    def editUser(request):
    """
    Edit Account Model
    """
    request_data = request.POST
    user = request.user
    account = Account.objects.get(user=user)
    interests = request_data.get("interests", False)
    if interests:
    interests = list(interests)
    else:
    interests = account.interests
    data = {
    "first_name": request_data.get("first_name", account.first_name),
    "last_name": request_data.get("last_name", account.last_name),
    "about_me": request_data.get("about_me", account.about_me),
    }
    account.__dict__.update(data)
    try:
    account.save()
    except Exception as e:
    return HttpResponseServerError(reason=str(e))
    account.refresh_from_db()
    return JsonResponse(Account.objects.summarize(account))
    @login_required
    def uploadProfileImage(request):
    """ This function is used to allow users to upload profile photos """
    if request.method == "POST":
    form = UpdateProfileImage(request.POST, request.FILES)
    if form.is_valid():
    try:
    account = Account.objects.get(user=request.user)
    # Clean up previous image
    account.profile_image.delete()
    # Upload new image and set as profile picture
    account.profile_image = form.clean_profile_image()
    try:
    account.save()
    except Exception as e:
    response = {"message": str(e), "error": "MODEL_SAVE_ERROR"}
    return JsonResponse(response, status=400)
    request.session["login_user_image"] = account.profile_image_thumb_url
    response = {"profile_image": account.profile_image_url}
    return JsonResponse(response, status=200)
    except Exception as e:
    response = {"message": str(e), "error": "MODEL_ERROR"}
    return JsonResponse(response, status=400)
    else:
    response = {"message": form.errors["profile_image"], "error": "FORM_ERROR"}
    return JsonResponse(response, status=400)
    else:
    return HttpResponseForbidden("allowed only via POST")
    @login_required
    def clearProfileImage(request):
    """ This function is used to delete a profile image """
    if request.method == "POST":
    try:
    account = Account.objects.get(user=request.user)
    # Clean up previous image
    account.profile_image.delete()
    account.save()
    return HttpResponse("Image Deleted")
    except Exception:
    return HttpResponseServerError(reason=str("default"))
    else:
    return HttpResponseForbidden("allowed only via POST")
    @login_required
    def uploadCiviImage(request):
    """This function is used to upload an image for a Civi"""
    if request.method == "POST":
    r = request.POST
    civi_id = r.get("civi_id")
    if not civi_id:
    return HttpResponseBadRequest(reason="Invalid Civi Reference")
    try:
    c = Civi.objects.get(id=civi_id)
    attachment_links = request.POST.getlist("attachment_links[]")
    if attachment_links:
    for img_link in attachment_links:
    result = urllib.urlretrieve(img_link)
    img_file = File(open(result[0]))
    if check_image_with_pil(img_file):
    civi_image = CiviImage(title="", civi=c, image=img_file)
    civi_image.save()
    if len(request.FILES) != 0:
    for image in request.FILES.getlist("attachment_image"):
    civi_image = CiviImage(title="", civi=c, image=image)
    civi_image.save()
    data = {
    "attachments": [
    {"id": img.id, "image_url": img.image_url} for img in c.images.all()
    ],
    }
    return JsonResponse(data)
    except Exception as e:
    return HttpResponseServerError(
    reason=(str(e) + civi_id + str(request.FILES))
    )
    else:
    return HttpResponseForbidden("allowed only via POST")
    def check_image_with_pil(image_file):
    """This function uses the PIL library to make sure the image format is supported"""
    try:
    PIL.Image.open(image_file)
    except IOError:
    return False
    return True
    @login_required
    def uploadThreadImage(request):
    """This function is used to upload an image to a thread"""
    if request.method == "POST":
    r = request.POST
    thread_id = r.get("thread_id")
    if not thread_id:
    return HttpResponseBadRequest(reason="Invalid Thread Reference")
    try:
    thread = Thread.objects.get(id=thread_id)
    remove = r.get("remove", "")
    img_link = r.get("link", "")
    if remove:
    thread.image.delete()
    thread.save()
    elif img_link:
    thread.image.delete()
    result = urllib.urlretrieve(img_link)
    img_file = File(open(result[0]))
    if check_image_with_pil(img_file):
    thread.image = img_file
    thread.save()
    # else:
    # return HttpResponseBadRequest("Invalid Image")
    else:
    # Clean up previous image
    thread.image.delete()
    # Upload new image and set as profile picture
    thread.image = request.FILES["attachment_image"]
    thread.save()
    data = {"image": thread.image_url}
    return JsonResponse(data)
    except Exception as e:
    return HttpResponseServerError(reason=(str(e)))
    else:
    return HttpResponseForbidden("allowed only via POST")
    @login_required
    @require_post_params(params=["target"])
    def requestFollow(request):
    """
    USAGE:
    Takes in user_id from current friend_requests list and joins accounts as friends.
    Does not join accounts as friends unless the POST friend is a valid member of the friend request array.
    Text POST:
    friend
    :return: (200, okay, list of friend information) (400, bad lookup) (500, error)
    """
    if request.user.username == request.POST.get("target", -1):
    return HttpResponseBadRequest(reason="You cannot follow yourself, silly!")
    try:
    account = Account.objects.get(user=request.user)
    target = User.objects.get(username=request.POST.get("target", -1))
    target_account = Account.objects.get(user=target)
    account.following.add(target_account)
    account.save()
    target_account.followers.add(account)
    target_account.save()
    data = {"username": target.username, "follow_status": True}
    notify.send(
    request.user, # Actor User
    recipient=target, # Target User
    verb=u"is following you", # Verb
    target=target_account, # Target Object
    popup_string="{user} is now following you".format(user=account.full_name),
    link="/{}/{}".format("profile", request.user.username),
    )
    return JsonResponse({"result": data})
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))
    except Exception as e:
    return HttpResponseServerError(reason=str(e))
    @login_required
    @require_post_params(params=["target"])
    def requestUnfollow(request):
    """
    USAGE:
    Takes in user_id from current friend_requests list and joins accounts as friends.
    Does not join accounts as friends unless the POST friend is a valid member of the friend request array.
    Text POST:
    friend
    :return: (200, okay, list of friend information) (400, bad lookup) (500, error)
    """
    try:
    account = Account.objects.get(user=request.user)
    target = User.objects.get(username=request.POST.get("target", -1))
    target_account = Account.objects.get(user=target)
    account.following.remove(target_account)
    account.save()
    target_account.followers.remove(account)
    target_account.save()
    return JsonResponse({"result": "Success"})
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))
    except Exception as e:
    return HttpResponseServerError(reason=str(e))
    @login_required
    def editUserCategories(request):
    """
    USAGE:
    Edits list of categories for the user
    """
    try:
    account = Account.objects.get(user=request.user)
    categories = [int(i) for i in request.POST.getlist("categories[]")]
    account.categories.clear()
    for category in categories:
    account.categories.add(Category.objects.get(id=category))
    account.save()
    data = {
    "user_categories": list(account.categories.values_list("id", flat=True))
    or "all_categories"
    }
    return JsonResponse({"result": data})
    except Account.DoesNotExist as e:
    return HttpResponseBadRequest(reason=str(e))
    except Exception as e:
    return HttpResponseServerError(reason=str(e))

@brylie
Copy link
Member Author

brylie commented Jun 14, 2021

@shashankks0987 would you be interested in taking this issue?

@shashankks0987
Copy link
Contributor

@brylie Yes I'd be happy to

@brylie brylie added the mentoring Issues that need active mentoring. label Jun 14, 2021
@brylie
Copy link
Member Author

brylie commented Jun 14, 2021

Thanks! I've assigned you.

I'll try and add a list of related code that I can find.

When working, try to figure out which code isn't needed anymore and remove it. If there is a more conventional way to do things with Django, replace any custom code with the conventional method

@NoriakiMawatari
Copy link

NoriakiMawatari commented Jun 15, 2021

Helloo @brylie, I'm very interested in contributing and would love to help with this issue or another good first issue! Please let me know what can I do for the team.

@RiddhiAthreya
Copy link

hey @brylie I'm new to contributing to open source and would love to contribute here! Let me know if there are issues I can contribute to!

@brylie
Copy link
Member Author

brylie commented Jun 15, 2021

Thank you for your offer @RiddhiAthreya and @NoriakiMawatari. Since @shashankks0987 is currently working on this task, we would need to see how you might assist them.

@shashankks0987 is there any way that @RiddhiAthreya and @NoriakiMawatari can assist you with this task?

@shashankks0987
Copy link
Contributor

I'm currently working on issue #849 as that was of higher priority. I thought I'll get back to this after that. If they are open to take it, you can assign them too and I can later continue from where they left off

@RiddhiAthreya
Copy link

@brylie can I be assigned this issue?

@brylie
Copy link
Member Author

brylie commented Jun 17, 2021

@RiddhiAthreya sure thing. You're assigned :-)

If you wouldn't mind, please check in with @shashankks0987 periodically as they are stewarding this task along with a couple of related tasks

@sandipan898
Copy link

Is this issue still open to fix, or someone have already fixed it?

@Adyyousf
Copy link

can i fix this issue?

@brylie
Copy link
Member Author

brylie commented Aug 20, 2021

@skattel49 still has a pull request in prorgress here and @wassafshahzad is working on related code. Let's try to avoid too many collisions in this area of the project.

@wassafshahzad, is there any way that @sandipan898 and @Adyyousf may assist you?

brylie added a commit that referenced this issue Sep 4, 2021
Moved Account and Profile code as per #926
@ghost
Copy link

ghost commented Sep 8, 2021

@brylie I am unable to find the account.py file . I am looking for my first contribution.

@brylie
Copy link
Member Author

brylie commented Sep 9, 2021

Ah, this task is basically complete.

@brylie brylie closed this as completed Sep 9, 2021
@brylie
Copy link
Member Author

brylie commented Sep 9, 2021

@Sonu03 would you be interested in helping with #231?

@gorkemarslan
Copy link
Collaborator

gorkemarslan commented Sep 9, 2021

@brylie is it possible to reopen this issue and assign me to it? Now I am writing unit testing for accounts app (issue #952) and some maintenance is needed to improve models.py file (not the models but codes in the file). While I am testing, I also want to fix some omitted problems because of moving other models to the accounts/models.py. For example, there are some code repetitions, etc.
Otherwise, should I create a new issue?

@brylie
Copy link
Member Author

brylie commented Sep 10, 2021

Otherwise, should I create a new issue?

Yes, for the sake of clarity, please create a new issue outlining the planned changes. I'll make sure you are assigned to the new issue.

@ghost
Copy link

ghost commented Sep 10, 2021

@brylie yes, I will check out issue #231

brylie pushed a commit that referenced this issue Sep 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants