Skip to content

Commit

Permalink
feat: add links feature
Browse files Browse the repository at this point in the history
  • Loading branch information
pablolmedorado committed May 17, 2021
1 parent 61e3a69 commit f50e6a8
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 17 deletions.
50 changes: 38 additions & 12 deletions backend/common/admin.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _

from import_export.admin import ImportExportActionModelAdmin
from ordered_model.admin import OrderedModelAdmin
from taggit.models import Tag

from .behaviors import UUIDTaggedItem


class TaggedItemInline(admin.StackedInline):
model = UUIDTaggedItem


class TagAdmin(admin.ModelAdmin):
inlines = [TaggedItemInline]
list_display = ["name", "slug"]
ordering = ["name", "slug"]
search_fields = ["name"]
prepopulated_fields = {"slug": ["name"]}
from .models import Link, LinkType
from .resources import LinkResource, LinkTypeResource, TagResource


@admin.register(LinkType)
class LinkTypeAdmin(ImportExportActionModelAdmin, OrderedModelAdmin):
list_display = ("id", "name", "move_up_down_links")
list_display_links = ("name",)
search_fields = ("name",)
ordering = ("order",)
fieldsets = ((_("Información básica"), {"fields": ("name",)}),)
resource_class = LinkTypeResource


@admin.register(Link)
class LinkAdmin(ImportExportActionModelAdmin, OrderedModelAdmin):
list_display = ("id", "name", "icon", "url", "type", "move_up_down_links")
list_select_related = ("type",)
search_fields = ("name", "url")
list_filter = ("type",)
ordering = ("type", "order")
fieldsets = (
(_("Información básica"), {"fields": ("name", "url")}),
(_("Clasificación"), {"fields": ("type",)}),
(_("Apariencia"), {"fields": ("icon",)}),
)
resource_class = LinkResource


class TagAdmin(ImportExportActionModelAdmin):
list_display = ("name", "slug")
ordering = ("name", "slug")
search_fields = ("name",)
prepopulated_fields = {"slug": ("name",)}
resource_class = TagResource


admin.site.unregister(Tag)
Expand Down
18 changes: 18 additions & 0 deletions backend/common/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework import serializers
from taggit.models import Tag

from ..models import Link, LinkType
from users.api.serializers import UserSerializer


Expand Down Expand Up @@ -43,3 +44,20 @@ class Meta:
"slug",
)
read_only_fields = fields


class LinkTypeSerializer(FlexFieldsModelSerializer):
class Meta:
model = LinkType
fields = ("id", "name", "order")
read_only_fields = fields


class LinkSerializer(FlexFieldsModelSerializer):
class Meta:
model = Link
fields = ("id", "name", "icon", "url", "type", "order")
read_only_fields = fields
expandable_fields = {
"type": LinkTypeSerializer,
}
13 changes: 11 additions & 2 deletions backend/common/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
AtomicUpdateModelMixin,
)
from .permissions import NotificationPermission
from .serializers import NotificationSerializer, TagSerializer
from .serializers import LinkSerializer, NotificationSerializer, TagSerializer
from ..models import Link


class AtomicModelViewSet(
Expand Down Expand Up @@ -98,9 +99,17 @@ def mark_all_as_unread(self, request, *args, **kwargs):


class TagViewSet(FlexFieldsMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TagSerializer
queryset = Tag.objects.all()
serializer_class = TagSerializer
filterset_class = TagFilterSet
search_fields = ("name",)
ordering_fields = ("id", "name")
ordering = ("name",)


class LinkViewSet(FlexFieldsMixin, viewsets.ReadOnlyModelViewSet):
queryset = Link.objects.all()
serializer_class = LinkSerializer
permit_list_expands = ["type"]
search_fields = ("name", "url")
ordering_fields = ()
58 changes: 58 additions & 0 deletions backend/common/migrations/0002_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("common", "0001_initial"),
]

operations = [
migrations.CreateModel(
name="LinkType",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("order", models.PositiveIntegerField(db_index=True, editable=False, verbose_name="order")),
("name", models.CharField(max_length=200, unique=True, verbose_name="nombre")),
],
options={
"verbose_name": "tipo de enlace",
"verbose_name_plural": "tipos de enlace",
"ordering": ("order",),
"abstract": False,
},
),
migrations.CreateModel(
name="Link",
fields=[
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("order", models.PositiveIntegerField(db_index=True, editable=False, verbose_name="order")),
("name", models.CharField(max_length=2000, unique=True, verbose_name="nombre")),
(
"icon",
models.CharField(
help_text="<a href='https://materialdesignicons.com/' target='_blank'>Material Design Icons</a>",
max_length=50,
verbose_name="icono en la aplicación",
),
),
("url", models.CharField(max_length=2000, unique=True, verbose_name="url")),
(
"type",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="links",
to="common.linktype",
verbose_name="tipo",
),
),
],
options={
"verbose_name": "enlace",
"verbose_name_plural": "enlaces",
"ordering": ("order",),
"abstract": False,
},
),
]
38 changes: 38 additions & 0 deletions backend/common/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _

from ordered_model.models import OrderedModel as Orderable


class LinkType(Orderable, models.Model):
name = models.CharField(_("nombre"), max_length=200, blank=False, unique=True)

def __str__(self):
return self.name

class Meta(Orderable.Meta):
verbose_name = _("tipo de enlace")
verbose_name_plural = _("tipos de enlace")


class Link(Orderable, models.Model):
name = models.CharField(_("nombre"), max_length=2000, blank=False, unique=True)
icon = models.CharField(
_("icono en la aplicación"),
help_text="<a href='https://materialdesignicons.com/' target='_blank'>Material Design Icons</a>",
max_length=50,
blank=False,
)
url = models.CharField(_("url"), max_length=2000, blank=False, unique=True)
type = models.ForeignKey(
LinkType, verbose_name=_("tipo"), related_name="links", blank=False, null=False, on_delete=models.PROTECT,
)

order_with_respect_to = ("type",)

def __str__(self):
return self.name

class Meta(Orderable.Meta):
verbose_name = _("enlace")
verbose_name_plural = _("enlaces")
29 changes: 29 additions & 0 deletions backend/common/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from import_export import resources
from import_export.fields import Field
from import_export.widgets import ForeignKeyWidget
from taggit.models import Tag

from .models import Link, LinkType


class LinkResource(resources.ModelResource):
type = Field(attribute="type", widget=ForeignKeyWidget(model=LinkType, field="name"), readonly=False)

class Meta:
model = Link
fields = ("id", "name", "icon", "url", "type", "order")
export_order = fields


class LinkTypeResource(resources.ModelResource):
class Meta:
model = LinkType
fields = ("id", "name", "order")
export_order = fields


class TagResource(resources.ModelResource):
class Meta:
model = Tag
fields = ("id", "name", "slug")
export_order = fields
8 changes: 8 additions & 0 deletions backend/common/static/js/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ if (workbox) {
var APP_INDEX = "app-index";
var STATIC_RESOURCES = "static-resources";
var NOTIFICATIONS = "notifications";
var LINKS = "links";
var SELECT_OPTIONS = "select-options";

workbox.setConfig({
Expand Down Expand Up @@ -50,6 +51,13 @@ if (workbox) {
})
);

workbox.routing.registerRoute(
new RegExp("/api/common/links/"),
new workbox.strategies.StaleWhileRevalidate({
cacheName: LINKS,
})
);

workbox.routing.registerRoute(
new RegExp("/api/users/users/"),
new workbox.strategies.NetworkFirst({
Expand Down
3 changes: 2 additions & 1 deletion backend/common/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from rest_framework_bulk.routes import BulkRouter

from .api.viewsets import NotificationViewSet, TagViewSet
from .api.viewsets import LinkViewSet, NotificationViewSet, TagViewSet

app_name = "common"

router = BulkRouter()
router.register(r"links", LinkViewSet, basename="link")
router.register(r"notifications", NotificationViewSet, basename="notification")
router.register(r"tags", TagViewSet, basename="tag")

Expand Down
1 change: 1 addition & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
</span>
<v-spacer />
<v-progress-circular v-show="loading" class="mr-5" :size="36" color="white" indeterminate />
<LinkManager />
<NotificationManager class="mr-5" />
<v-menu bottom left offset-y>
<template #activator="{ on, attrs }">
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/components/common/LinkManager.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<template>
<v-menu
v-model="showMenu"
content-class="v-dialog--scrollable"
:min-width="400"
:max-width="400"
:close-on-content-click="false"
:nudge-width="200"
bottom
left
offset-y
>
<template #activator="{ on: menu }">
<v-tooltip bottom>
<template #activator="{ on: tooltip }">
<v-btn v-show="$vuetify.breakpoint.smAndUp" :disabled="loading" icon v-on="{ ...tooltip, ...menu }">
<v-icon>mdi-apps</v-icon>
</v-btn>
</template>
<span> Enlaces </span>
</v-tooltip>
</template>

<v-card>
<v-card-text>
<template v-for="(links, type, index) in itemsByType">
<v-divider v-if="index" :key="`${type}-divider`" class="my-2"></v-divider>
<v-row :key="type" dense>
<v-col v-for="link in links" :key="link.name" cols="4">
<v-hover v-slot="{ hover }">
<v-card :href="link.url" target="_blank" class="pa-2" flat ripple>
<div class="d-flex justify-center">
<v-icon x-large :color="hover ? 'secondary' : 'default'">{{ link.icon }}</v-icon>
</div>
<div class="d-flex justify-center text-center">
<span :class="`${hover ? 'secondary' : 'default'}--text text-truncate`">
{{ link.name }}
</span>
</div>
</v-card>
</v-hover>
</v-col>
</v-row>
</template>
</v-card-text>
</v-card>
</v-menu>
</template>

<script>
import { groupBy } from "lodash";
import LinkService from "@/services/common/link-service";
export default {
name: "LinkManager",
data() {
return {
showMenu: false,
loading: false,
items: [],
};
},
computed: {
itemsByType() {
return groupBy(this.items, "type.order");
},
},
created() {
this.fetchItems();
},
methods: {
async fetchItems() {
try {
this.loading = true;
const response = await LinkService.list({ expand: "type" });
this.items = response.data;
} finally {
this.loading = false;
}
},
},
};
</script>

<style scoped>
.v-dialog--scrollable {
max-height: 80%;
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/plugins/reverse.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion frontend/src/services/common/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { keyBy } from "lodash";

import LinkService from "./link-service";
import NotificationService from "./notification-service";
import TagService from "./tag-service";

const services = keyBy([NotificationService, TagService], "baseUrlName");
const services = keyBy([LinkService, NotificationService, TagService], "baseUrlName");

export default services;
9 changes: 9 additions & 0 deletions frontend/src/services/common/link-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import BaseService from "../base-service";

class LinkService extends BaseService {
baseUrlName = "common:link";
}

const service = Object.freeze(new LinkService());

export default service;

0 comments on commit f50e6a8

Please sign in to comment.