API

User Dashboard module

serializers

ModerationCommentSerializer

Bases: ModelSerializer

Source code in apps/userdashboard/serializers.py
class ModerationCommentSerializer(serializers.ModelSerializer):
    comment_url = serializers.SerializerMethodField()
    is_unread = serializers.SerializerMethodField()
    is_modified = serializers.SerializerMethodField()
    last_edit = serializers.SerializerMethodField()
    moderator_feedback = ModeratorCommentFeedbackSerializer(read_only=True)
    num_reports = serializers.SerializerMethodField()
    feedback_api_url = serializers.SerializerMethodField()
    user_name = serializers.SerializerMethodField()
    user_image = serializers.SerializerMethodField()
    user_profile_url = serializers.SerializerMethodField()

    class Meta:
        model = Comment
        fields = [
            "comment",
            "comment_url",
            "feedback_api_url",
            "is_unread",
            "is_blocked",
            "is_moderator_marked",
            "is_modified",
            "last_edit",
            "moderator_feedback",
            "num_reports",
            "pk",
            "user_image",
            "user_name",
            "user_profile_url",
        ]

    def get_comment_url(self, instance):
        return instance.get_absolute_url()

    def get_is_modified(self, comment):
        return comment.modified is not None

    def get_last_edit(self, comment):
        if comment.modified:
            return get_date_display(comment.modified)
        else:
            return get_date_display(comment.created)

    def get_feedback_api_url(self, comment):
        return reverse("moderatorfeedback-list", kwargs={"comment_pk": comment.pk})

    def get_num_reports(self, comment):
        return comment.num_reports

    def get_user_name(self, comment):
        if comment.is_censored or comment.is_removed:
            return _("unknown user")
        return str(comment.creator.username)

    def get_user_image_fallback(self, comment):
        """Load small thumbnail images for default user images."""
        if comment.is_censored or comment.is_removed:
            return None
        try:
            if comment.creator.avatar_fallback:
                return comment.creator.avatar_fallback
        except AttributeError:
            pass
        return None

    def get_user_image(self, comment):
        """Load small thumbnail images for user images."""
        if comment.is_censored or comment.is_removed:
            return None
        try:
            if comment.creator.avatar:
                avatar = get_thumbnailer(comment.creator.avatar)["avatar"]
                return avatar.url
        except AttributeError:
            pass
        return self.get_user_image_fallback(comment)

    def get_user_profile_url(self, comment):
        if comment.is_censored or comment.is_removed:
            return ""
        try:
            return comment.creator.get_absolute_url()
        except AttributeError:
            return ""

    def get_is_unread(self, comment):
        return not comment.is_reviewed

    def update(self, instance, validated_data):
        """Update comment instance without changing comment.modified.

        This is essentially copied from
        rest_framework.serializers.ModelSerializer.update(),
        only difference is ignore_modified=true when saving the instance.
        See also here:
        https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L991-L1015
        """
        raise_errors_on_nested_writes("update", self, validated_data)
        info = model_meta.get_field_info(instance)

        # Simply set each attribute on the instance, and then save it.
        # Note that unlike `.create()` we don't need to treat many-to-many
        # relationships as being a special case. During updates we already
        # have an instance pk for the relationships to be associated with.
        m2m_fields = []
        for attr, value in validated_data.items():
            if attr in info.relations and info.relations[attr].to_many:
                m2m_fields.append((attr, value))
            else:
                setattr(instance, attr, value)

        instance.save(ignore_modified=True)

        # Note that many-to-many fields are set after updating instance.
        # Setting m2m fields triggers signals which could potentially change
        # updated instance and we do not want it to collide with .update()
        for attr, value in m2m_fields:
            field = getattr(instance, attr)
            field.set(value)

        return instance

get_user_image(comment)

Load small thumbnail images for user images.

Source code in apps/userdashboard/serializers.py
def get_user_image(self, comment):
    """Load small thumbnail images for user images."""
    if comment.is_censored or comment.is_removed:
        return None
    try:
        if comment.creator.avatar:
            avatar = get_thumbnailer(comment.creator.avatar)["avatar"]
            return avatar.url
    except AttributeError:
        pass
    return self.get_user_image_fallback(comment)

get_user_image_fallback(comment)

Load small thumbnail images for default user images.

Source code in apps/userdashboard/serializers.py
def get_user_image_fallback(self, comment):
    """Load small thumbnail images for default user images."""
    if comment.is_censored or comment.is_removed:
        return None
    try:
        if comment.creator.avatar_fallback:
            return comment.creator.avatar_fallback
    except AttributeError:
        pass
    return None

update(instance, validated_data)

Update comment instance without changing comment.modified.

This is essentially copied from rest_framework.serializers.ModelSerializer.update(), only difference is ignore_modified=true when saving the instance. See also here: https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L991-L1015

Source code in apps/userdashboard/serializers.py
def update(self, instance, validated_data):
    """Update comment instance without changing comment.modified.

    This is essentially copied from
    rest_framework.serializers.ModelSerializer.update(),
    only difference is ignore_modified=true when saving the instance.
    See also here:
    https://github.com/encode/django-rest-framework/blob/master/rest_framework/serializers.py#L991-L1015
    """
    raise_errors_on_nested_writes("update", self, validated_data)
    info = model_meta.get_field_info(instance)

    # Simply set each attribute on the instance, and then save it.
    # Note that unlike `.create()` we don't need to treat many-to-many
    # relationships as being a special case. During updates we already
    # have an instance pk for the relationships to be associated with.
    m2m_fields = []
    for attr, value in validated_data.items():
        if attr in info.relations and info.relations[attr].to_many:
            m2m_fields.append((attr, value))
        else:
            setattr(instance, attr, value)

    instance.save(ignore_modified=True)

    # Note that many-to-many fields are set after updating instance.
    # Setting m2m fields triggers signals which could potentially change
    # updated instance and we do not want it to collide with .update()
    for attr, value in m2m_fields:
        field = getattr(instance, attr)
        field.set(value)

    return instance

views

UserDashboardActivitiesView

Bases: UserDashboardBaseMixin

Source code in apps/userdashboard/views.py
class UserDashboardActivitiesView(UserDashboardBaseMixin):
    template_name = "a4_candy_userdashboard/userdashboard_activities.html"
    menu_item = "overview"

    @property
    def actions(self):
        """Return comment/feedback actions that are  on content the user created.

        Do not return actions on comments for polls and documents to not spam
        initiators.
        """
        user = self.request.user
        comment_actions = (
            Action.objects.filter(
                obj_content_type=ContentType.objects.get_for_model(Comment),
                verb="add",
                target_creator=user,
            )
            .exclude(
                target_content_type__in=[
                    ContentType.objects.get_for_model(Poll),
                    ContentType.objects.get_for_model(Chapter),
                    ContentType.objects.get_for_model(Paragraph),
                ]
            )
            .exclude(
                actor=user,
            )
            .select_related("actor", "project")
            .prefetch_related("obj", "target__creator")
        )

        filtered_comment_actions = [
            action for action in comment_actions if not action.obj.is_blocked
        ]
        feedback_actions = (
            Action.objects.filter(
                obj_content_type=ContentType.objects.get_for_model(
                    ModeratorCommentFeedback
                ),
                obj_comment_creator=user,
            )
            .exclude(actor=user)
            .select_related("project")
            .prefetch_related("obj__comment__creator")
        )

        return sorted(
            filtered_comment_actions + list(feedback_actions),
            key=lambda action: action.timestamp,
            reverse=True,
        )

actions property

Return comment/feedback actions that are on content the user created.

Do not return actions on comments for polls and documents to not spam initiators.

UserDashboardBaseMixin

Bases: LoginRequiredMixin, ContextMixin, TemplateResponseMixin, View

Adds followed projects and organisations as properties.

To be used in the user dashboard views, as they all need this info.

Source code in apps/userdashboard/views.py
class UserDashboardBaseMixin(
    LoginRequiredMixin,
    generic.base.ContextMixin,
    generic.base.TemplateResponseMixin,
    generic.base.View,
):
    """
    Adds followed projects and organisations as properties.

    To be used in the user dashboard views, as they all need this info.
    """

    model = User

    def get(self, request):
        response = self.render_to_response(self.get_context_data())
        return response

    @property
    def organisations(self):
        return Organisation.objects.filter(
            project__follow__creator=self.request.user, project__follow__enabled=True
        ).distinct()

    @property
    def projects(self):
        projects = Project.objects.filter(
            follow__creator=self.request.user, follow__enabled=True
        )
        return projects

UserDashboardOverviewView

Bases: UserDashboardBaseMixin

Source code in apps/userdashboard/views.py
class UserDashboardOverviewView(UserDashboardBaseMixin):
    template_name = "a4_candy_userdashboard/userdashboard_overview.html"
    menu_item = "overview"

    @property
    def actions(self):
        """Return comment/feedback actions that are  on content the user created.

        Do not return actions on comments for polls and documents to not spam
        initiators.
        """
        user = self.request.user
        comment_actions = (
            Action.objects.filter(
                obj_content_type=ContentType.objects.get_for_model(Comment),
                verb="add",
                target_creator=user,
            )
            .exclude(
                target_content_type__in=[
                    ContentType.objects.get_for_model(Poll),
                    ContentType.objects.get_for_model(Chapter),
                    ContentType.objects.get_for_model(Paragraph),
                ]
            )
            .exclude(actor=user)
            .select_related("actor", "project")
            .prefetch_related("obj", "target__creator")
        )
        filtered_comment_actions = [
            action for action in comment_actions if not action.obj.is_blocked
        ]
        feedback_actions = (
            Action.objects.filter(
                obj_content_type=ContentType.objects.get_for_model(
                    ModeratorCommentFeedback
                ),
                obj_comment_creator=user,
            )
            .exclude(actor=user)
            .select_related("project", "project__organisation")
            .prefetch_related("obj__comment__creator", "obj__comment__content_object")
        )

        return sorted(
            filtered_comment_actions + list(feedback_actions),
            key=lambda action: action.timestamp,
            reverse=True,
        )

    @property
    def projects_carousel(self):
        (
            sorted_active_projects,
            sorted_future_projects,
            sorted_past_projects,
        ) = self.request.user.get_projects_follow_list()
        projects = (
            list(sorted_active_projects)
            + list(sorted_future_projects)
            + list(sorted_past_projects)
        )[:8]

        return projects

actions property

Return comment/feedback actions that are on content the user created.

Do not return actions on comments for polls and documents to not spam initiators.

users and permissions

We enabled token authentification in a+. So to use the APIs, the user token has to be given instead of name and password.

To generate or get the token in the terminal: curl -X POST http://localhost:8004/api/login/ -H 'Accept: application/json;' -d 'username=$username&password=$password' {"token":"$some_quite_long_token"}

Idea API

To create, update and delete ideas from the app, we added an API for ideas.

To use it to add an idea: curl -X POST http://localhost:8004/api/modules/$moduleId/ideas/ -H 'Accept: application/json;' -H 'Authorization: Token $some_quite_long_token' -d 'name=my name&description=my description'

If the idea, requires a category, use it with the pk of the category: curl -X POST http://localhost:8004/api/modules/$moduleId/ideas/ -H 'Accept: application/json;' -H 'Authorization: Token $some_quite_long_token' -d 'name=my name&description=my description&category=1'

To post many ideas very quickly, use a shell-script like this one:

for i in {1..10}
do
curl -X POST http://localhost:8004/api/modules/$moduleId/ideas/ -H 'Accept: application/json;' -H 'Authorization: Token $some_quite_long_token' -d 'name=my name&description=my description&category=1'
done