diff --git a/openwisp_utils/__init__.py b/openwisp_utils/__init__.py new file mode 100644 index 00000000..3cf46865 --- /dev/null +++ b/openwisp_utils/__init__.py @@ -0,0 +1,14 @@ +VERSION = (0, 1, 0, 'alpha') +__version__ = VERSION # alias + + +def get_version(): + version = '%s.%s.%s' % (VERSION[0], VERSION[1], VERSION[2]) + if VERSION[3] != 'final': + first_letter = VERSION[3][0:1] + try: + suffix = VERSION[4] + except IndexError: + suffix = 0 + version = '%s%s%s' % (version, first_letter, suffix) + return version diff --git a/openwisp_utils/admin.py b/openwisp_utils/admin.py new file mode 100644 index 00000000..30384762 --- /dev/null +++ b/openwisp_utils/admin.py @@ -0,0 +1,98 @@ +from django.contrib import admin +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ + + +class MultitenantAdminMixin(object): + """ + Mixin that makes a ModelAdmin class multitenant: + users will see only the objects related to the organizations + they are associated with. + """ + multitenant_shared_relations = [] + + def get_repr(self, obj): + return str(obj) + + get_repr.short_description = _('name') + + def get_queryset(self, request): + """ + If current user is not superuser, show only the + objects associated to organizations he/she is associated with + """ + qs = super(MultitenantAdminMixin, self).get_queryset(request) + if request.user.is_superuser: + return qs + organizations = request.user.organizations_pk + return qs.filter(organization__in=organizations) + + def _edit_form(self, request, form): + """ + Modifies the form querysets as follows; + if current user is not superuser: + * show only relevant organizations + * show only relations associated to relevant organizations + or shared relations + else show everything + """ + fields = form.base_fields + if not request.user.is_superuser: + orgs_pk = request.user.organizations_pk + # organizations relation; + # may be readonly and not present in field list + if 'organization' in fields: + org_field = fields['organization'] + org_field.queryset = org_field.queryset.filter(pk__in=orgs_pk) + # other relations + q = Q(organization__in=orgs_pk) | Q(organization=None) + for field_name in self.multitenant_shared_relations: + # each relation may be readonly + # and not present in field list + if field_name not in fields: + continue + field = fields[field_name] + field.queryset = field.queryset.filter(q) + + def get_form(self, request, obj=None, **kwargs): + form = super(MultitenantAdminMixin, self).get_form(request, obj, **kwargs) + self._edit_form(request, form) + return form + + def get_formset(self, request, obj=None, **kwargs): + formset = super(MultitenantAdminMixin, self).get_formset(request, obj=None, **kwargs) + self._edit_form(request, formset.form) + return formset + + +class MultitenantOrgFilter(admin.RelatedFieldListFilter): + """ + Admin filter that shows only organizations the current + user is associated with in its available choices + """ + multitenant_lookup = 'pk__in' + + def field_choices(self, field, request, model_admin): + if request.user.is_superuser: + return super(MultitenantOrgFilter, self).field_choices(field, request, model_admin) + organizations = request.user.organizations_pk + return field.get_choices(include_blank=False, + limit_choices_to={self.multitenant_lookup: organizations}) + + +class MultitenantObjectFilter(MultitenantOrgFilter): + """ + Admin filter that shows only objects of + organizations the current user is associated with + """ + multitenant_lookup = 'organization__in' + + +class TimeReadonlyAdminMixin(object): + """ + mixin that automatically flags + `created` and `modified` as readonly + """ + def __init__(self, *args, **kwargs): + self.readonly_fields += ('created', 'modified',) + super(TimeReadonlyAdminMixin, self).__init__(*args, **kwargs) diff --git a/openwisp_utils/admin_theme/__init__.py b/openwisp_utils/admin_theme/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openwisp_utils/admin_theme/admin.py b/openwisp_utils/admin_theme/admin.py new file mode 100644 index 00000000..875f8a27 --- /dev/null +++ b/openwisp_utils/admin_theme/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from django.utils.translation import ugettext_lazy + + +def openwisp_admin(site_url=None): + # + admin.site.site_title = ugettext_lazy('OpenWISP2 Admin') + # link to frontend + admin.site.site_url = site_url + # h1 text + admin.site.site_header = ugettext_lazy('OpenWISP') + # text at the top of the admin index page + admin.site.index_title = ugettext_lazy('Network administration') diff --git a/openwisp_utils/admin_theme/static/admin/css/openwisp.css b/openwisp_utils/admin_theme/static/admin/css/openwisp.css new file mode 100644 index 00000000..1d12c4b4 --- /dev/null +++ b/openwisp_utils/admin_theme/static/admin/css/openwisp.css @@ -0,0 +1,102 @@ +#header{ + background-color: #464646; + color: #fff; + height: auto +} + +#user-tools{ margin-top: 12px } + +#site-name a{ + display: block; + width: 150px; + height: 70px; + background: url(../../../static/ui/openwisp/images/openwisp-logo.svg) no-repeat scroll 0 50% / 100%; + text-indent: -2000px; + overflow: hidden; +} + +.login #branding{ + float: none; + padding: 8px 0 +} +.login #site-name a{ margin: 0 auto } + +.button.default, +input[type=submit].default, +.submit-row input.default, +.object-tools a:focus, +.object-tools a:hover{ + background-color: #333; +} + +.button.default:active, input[type=submit].default:active, +.button.default:focus, input[type=submit].default:focus, +.button.default:hover, input[type=submit].default:hover{ + background-color: #000; +} + +#branding h1, #branding h1 a:link, #branding h1 a:visited { + color: #fff; +} + +div.breadcrumbs, +.module h2, .module caption, .inline-group h2, +#container .selector-chosen h2{ + background-color: #df5d43; + color: #fff; +} + +div.breadcrumbs{ + color: #ffcbc0 +} + +div.breadcrumbs a:focus, div.breadcrumbs a:hover{ + color: #ffe8e3 +} + +.button, input[type=submit], input[type=button], .submit-row input, a.button{ + background-color: #666; +} + +.button:active, input[type=submit]:active, input[type=button]:active, +.button:focus, input[type=submit]:focus, input[type=button]:focus, +.button:hover, input[type=submit]:hover, input[type=button]:hover { + background-color: #444; +} + +#user-tools a:focus, #user-tools a:hover{ + border-bottom-color: #ffcbc0; + color: #ffcbc0; +} + +#container .object-tools a{ + padding: 5px 15px; +} + +#container .object-tools a.addlink{ padding-right: 28px } + +table thead th.sorted .sortoptions a.sortremove:focus:after, +table thead th.sorted .sortoptions a.sortremove:hover:after, +a:link, a:visited { + color: #777; +} + +a:focus, a:hover { + color: #000; +} + +#changelist-filter li.selected a{ + color: #df5d43 !important +} + +#netjsonconfig-hint, #netjsonconfig-hint a, +#container .field-backend a{ color: #df5d43 } + +/* temporary frontend fix */ +a.button.secondaryAction{ + background: transparent; + border: 0 none; + padding: 0; + margin: 15px 0; + display: block; +} diff --git a/openwisp_utils/admin_theme/static/ui/openwisp/images/favicon.png b/openwisp_utils/admin_theme/static/ui/openwisp/images/favicon.png new file mode 100644 index 00000000..2f1b50f5 Binary files /dev/null and b/openwisp_utils/admin_theme/static/ui/openwisp/images/favicon.png differ diff --git a/openwisp_utils/admin_theme/static/ui/openwisp/images/openwisp-logo.svg b/openwisp_utils/admin_theme/static/ui/openwisp/images/openwisp-logo.svg new file mode 100644 index 00000000..f038e331 --- /dev/null +++ b/openwisp_utils/admin_theme/static/ui/openwisp/images/openwisp-logo.svg @@ -0,0 +1,139 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + version="1.1" + width="1377.5" + height="744.09998" + id="svg2" + xml:space="preserve"><metadata + id="metadata8"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs + id="defs6"><linearGradient + id="linearGradient3133"><stop + id="stop3135" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /><stop + id="stop3137" + style="stop-color:#ffffff;stop-opacity:0" + offset="1" /></linearGradient><clipPath + id="clipPath20"><path + d="M 9169.55,5390.38 C 8629.64,5620.56 7567.6,3852.27 7070.11,2319.85 c 0,0 -157.36,397.54 -172.33,863.1 -8.76,270.49 64.66,188.1 94.66,170.06 l 10.8,80.58 -142.3,20.28 0,0.08 -147.19,17.16 -10.45,-80.7 c 33.74,9.95 125.67,70.85 48.15,-188.51 -133.41,-446.21 -387.1,-790.54 -387.1,-790.54 -89.64,1608.59 -664.72,3589.05 -1245.63,3504.08 -207.06,-30.28 -953.38,-460.39 -550.91,-1291.39 -0.15,-0.09 -0.35,-0.44 -0.42,-0.7 5.64,-11.81 12.22,-23.82 18.47,-35.76 4.41,-8.4 8.28,-16.45 12.82,-24.86 14.68,-27.22 30.75,-54.59 48,-82.56 5.04,-8.14 10.53,-16.6 16.09,-24.75 13.93,-22.23 29.2,-44.65 45.01,-67.32 6.12,-8.47 11.95,-17.27 18.26,-25.93 21.32,-29.43 43.99,-59.31 68.1,-89.37 4.75,-6.01 10.04,-12.02 15,-18.05 20.98,-25.4 43.04,-51.26 66.19,-77.46 8.01,-8.93 16.03,-18.01 24.3,-27.07 29.01,-31.69 59,-63.97 91.39,-96.37 30.34,-30.51 57.56,-59.22 86.37,-88.9 -0.49,0.58 -0.9,1.06 -1.38,1.64 -857.23,1010.69 -320.97,1499.92 -145.08,1608.81 494.37,305.49 1376.78,-2548.93 1370.95,-3374.75 192.41,217.5 367.55,474.1 500.88,823.21 39.72,-371.64 143.36,-664.37 273.8,-923.86 205.42,799.73 1787.56,3335 2187.57,2913.7 142.31,-150.09 534.94,-759.03 -548.95,-1516.77 33.38,20.13 67.27,40.29 102.17,60.72 39.58,23.32 76.57,46.58 112.85,70 10.38,6.76 20.42,13.33 30.6,19.99 29.07,19.49 56.96,38.73 83.58,57.98 6.31,4.58 12.91,9.03 19.07,13.67 31.22,23 60.68,46.04 88.87,68.99 8.36,6.79 16.01,13.71 24.31,20.35 21.05,17.96 41.47,35.85 60.82,53.57 7.19,6.69 14.79,13.38 21.64,19.93 23.86,22.59 46.46,45.01 67.63,67.62 6.45,6.96 12.23,13.77 18.74,20.88 8.95,9.76 18.52,19.86 27.07,29.87 -0.11,0.17 -0.31,0.58 -0.31,0.86 601.57,700.59 -10.33,1306.91 -202.67,1389.02 z" + id="path22" /></clipPath><radialGradient + cx="0" + cy="0" + r="1" + fx="0" + fy="0" + id="radialGradient24" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(29.6215,339.49,339.49,-29.6215,685.579,327.614)" + spreadMethod="pad"><stop + id="stop26" + style="stop-color:#ffffff;stop-opacity:1" + offset="0" /><stop + id="stop28" + style="stop-color:#1e1a1b;stop-opacity:1" + offset="1" /></radialGradient><clipPath + id="clipPath36"><path + d="M 9264.13,5013.7 C 8864.12,5435 7281.98,2899.73 7076.56,2100 c 7.19,-14.23 14.41,-28.77 21.72,-42.95 19.01,-34.55 49.69,-90.16 89.12,-161.16 C 7459.24,1405.24 8143.76,180.539 8248.59,94.0586 8341.98,17.0625 7918.51,634.57 7997.46,900.551 c 150.86,-190.969 271.01,-334.469 309.91,-360 5.03,-3.18 8.77,-4.871 10.91,-5.219 28.74,-3.902 -181.51,243.379 -262.71,448.731 38.23,26.487 94.16,39.387 173.33,33.257 216.43,-16.89 60.49,47.12 -154.12,205.3 36.74,24.58 95.92,35.84 184.79,27.77 453.63,-41.13 -737.51,241.5 -687.5,854.52 34.16,361.84 325.3,821.31 1140.54,1390.19 0.81,0.64 1.69,1.22 2.57,1.83 1083.89,757.74 691.26,1366.68 548.95,1516.77 z m -4333.2,561.7 c -175.89,-108.89 -712.15,-598.12 145.08,-1608.81 0.48,-0.58 0.89,-1.06 1.38,-1.64 641.87,-756.99 805.77,-1275.09 746.48,-1633.51 -108.35,-605.35 -1332.37,-574.93 -883.22,-650.77 87.71,-14.78 142.29,-40.8 171.35,-73.87 -247.6,-98.08 -414.95,-120.41 -201.22,-159.31 78.2,-14.2 128.77,-41.01 158.92,-76.46 -130.96,-177.58 -397.36,-363.2 -368.57,-366.64 2.1,-0.38 6.05,0.3 11.68,2.22 44.27,14.8 197.14,122.81 391.71,268.97 8.36,-277.381 -558.78,-766.33 -448.8,-715.678 123.75,56.95 1101.98,1070.048 1489,1473.578 54.03,56.44 96.68,101.04 123.7,129.33 11.25,12.36 22.34,25.19 33.46,37.84 5.83,825.82 -876.58,3680.24 -1370.95,3374.75 z" + id="path38" /></clipPath><radialGradient + cx="0" + cy="0" + r="1" + fx="0" + fy="0" + id="radialGradient40" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(259.498,0,0,-259.498,702.739,284.282)" + spreadMethod="pad"><stop + id="stop42" + style="stop-color:#fde3c3;stop-opacity:1" + offset="0" /><stop + id="stop44" + style="stop-color:#f7921a;stop-opacity:1" + offset="1" /></radialGradient><clipPath + id="clipPath52"><path + d="m 9168.6,4288.21 c -13.3,7.21 -27.81,11.88 -43.72,14.05 -430.07,56.53 -1789.26,-1741.53 -2017.92,-2132.37 -11.27,-23.43 -24.36,-54.51 -30.4,-69.89 42.91,-57.96 567.75,-752.17 920.5,-1198.891 10.31,34.77 28.86,62.981 58.37,83.231 -40.11,101.56 -48.52,192.94 19.23,238.44 -236.59,174.47 -544.27,463.57 -502.59,885.66 0.62,7.35 1.49,14.93 2.51,22.53 45.63,357.58 342.3,808.84 1138.03,1364.13 2.78,1.73 5.16,3.45 7.74,5.23 309.39,217.06 500.33,424.76 611.07,613.12 -45.42,85.99 -112.64,147.05 -162.82,174.76 z m -4284.71,563.82 c -15.89,1.99 -31.16,1.12 -45.76,-2.33 -55.66,-14.03 -136.45,-55.83 -202.17,-127.52 58.85,-210.26 190.44,-459.96 434.21,-748.67 1.98,-2.41 3.89,-4.64 5.84,-6.92 627.56,-739.83 799.26,-1251.77 752.06,-1609.13 -1.07,-7.61 -2.3,-15.21 -3.31,-22.68 -67.68,-418.52 -439.32,-619.55 -712.49,-728.18 53.97,-60.96 22.46,-147.17 -42.23,-235.16 23.21,-27.14 33.95,-59.27 35.17,-95.33 455.25,341.9 1140.58,879.32 1196.67,924.54 -1.84,16.42 -6.58,49.53 -11.41,75.06 -121.4,436.26 -976.25,2521.15 -1406.58,2576.32 z" + id="path54" /></clipPath><radialGradient + cx="0" + cy="0" + r="1" + fx="0" + fy="0" + id="radialGradient56" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(316.669,-49.216,-14.4726,-93.1204,680.989,292.924)" + spreadMethod="pad"><stop + id="stop58" + style="stop-color:#f9b2b4;stop-opacity:1" + offset="0" /><stop + id="stop60" + style="stop-color:#c4161d;stop-opacity:1" + offset="1" /></radialGradient><linearGradient + x1="2337.2021" + y1="1799.646" + x2="2290.4583" + y2="5340.5073" + id="linearGradient3139" + xlink:href="#linearGradient3133" + gradientUnits="userSpaceOnUse" /></defs><g + transform="matrix(1.25,0,0,-1.25,0,744.1)" + id="g10"><g + transform="scale(0.1,0.1)" + id="g12"><g + id="g16" + style="fill:url(#linearGradient3139);fill-opacity:1"><g + clip-path="url(#clipPath20)" + id="g18" + style="fill:url(#linearGradient3139);fill-opacity:1"><path + d="M 9169.55,5390.38 C 8629.64,5620.56 7567.6,3852.27 7070.11,2319.85 c 0,0 -157.36,397.54 -172.33,863.1 -8.76,270.49 64.66,188.1 94.66,170.06 l 10.8,80.58 -142.3,20.28 0,0.08 -147.19,17.16 -10.45,-80.7 c 33.74,9.95 125.67,70.85 48.15,-188.51 -133.41,-446.21 -387.1,-790.54 -387.1,-790.54 -89.64,1608.59 -664.72,3589.05 -1245.63,3504.08 -207.06,-30.28 -953.38,-460.39 -550.91,-1291.39 -0.15,-0.09 -0.35,-0.44 -0.42,-0.7 5.64,-11.81 12.22,-23.82 18.47,-35.76 4.41,-8.4 8.28,-16.45 12.82,-24.86 14.68,-27.22 30.75,-54.59 48,-82.56 5.04,-8.14 10.53,-16.6 16.09,-24.75 13.93,-22.23 29.2,-44.65 45.01,-67.32 6.12,-8.47 11.95,-17.27 18.26,-25.93 21.32,-29.43 43.99,-59.31 68.1,-89.37 4.75,-6.01 10.04,-12.02 15,-18.05 20.98,-25.4 43.04,-51.26 66.19,-77.46 8.01,-8.93 16.03,-18.01 24.3,-27.07 29.01,-31.69 59,-63.97 91.39,-96.37 30.34,-30.51 57.56,-59.22 86.37,-88.9 -0.49,0.58 -0.9,1.06 -1.38,1.64 -857.23,1010.69 -320.97,1499.92 -145.08,1608.81 494.37,305.49 1376.78,-2548.93 1370.95,-3374.75 192.41,217.5 367.55,474.1 500.88,823.21 39.72,-371.64 143.36,-664.37 273.8,-923.86 205.42,799.73 1787.56,3335 2187.57,2913.7 142.31,-150.09 534.94,-759.03 -548.95,-1516.77 33.38,20.13 67.27,40.29 102.17,60.72 39.58,23.32 76.57,46.58 112.85,70 10.38,6.76 20.42,13.33 30.6,19.99 29.07,19.49 56.96,38.73 83.58,57.98 6.31,4.58 12.91,9.03 19.07,13.67 31.22,23 60.68,46.04 88.87,68.99 8.36,6.79 16.01,13.71 24.31,20.35 21.05,17.96 41.47,35.85 60.82,53.57 7.19,6.69 14.79,13.38 21.64,19.93 23.86,22.59 46.46,45.01 67.63,67.62 6.45,6.96 12.23,13.77 18.74,20.88 8.95,9.76 18.52,19.86 27.07,29.87 -0.11,0.17 -0.31,0.58 -0.31,0.86 601.57,700.59 -10.33,1306.91 -202.67,1389.02" + id="path30" + style="fill:url(#linearGradient3139);fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g><g + id="g3130"><g + id="g32"><g + clip-path="url(#clipPath36)" + id="g34"><path + d="M 9264.13,5013.7 C 8864.12,5435 7281.98,2899.73 7076.56,2100 c 7.19,-14.23 14.41,-28.77 21.72,-42.95 19.01,-34.55 49.69,-90.16 89.12,-161.16 C 7459.24,1405.24 8143.76,180.539 8248.59,94.0586 8341.98,17.0625 7918.51,634.57 7997.46,900.551 c 150.86,-190.969 271.01,-334.469 309.91,-360 5.03,-3.18 8.77,-4.871 10.91,-5.219 28.74,-3.902 -181.51,243.379 -262.71,448.731 38.23,26.487 94.16,39.387 173.33,33.257 216.43,-16.89 60.49,47.12 -154.12,205.3 36.74,24.58 95.92,35.84 184.79,27.77 453.63,-41.13 -737.51,241.5 -687.5,854.52 34.16,361.84 325.3,821.31 1140.54,1390.19 0.81,0.64 1.69,1.22 2.57,1.83 1083.89,757.74 691.26,1366.68 548.95,1516.77 z m -4333.2,561.7 c -175.89,-108.89 -712.15,-598.12 145.08,-1608.81 0.48,-0.58 0.89,-1.06 1.38,-1.64 641.87,-756.99 805.77,-1275.09 746.48,-1633.51 -108.35,-605.35 -1332.37,-574.93 -883.22,-650.77 87.71,-14.78 142.29,-40.8 171.35,-73.87 -247.6,-98.08 -414.95,-120.41 -201.22,-159.31 78.2,-14.2 128.77,-41.01 158.92,-76.46 -130.96,-177.58 -397.36,-363.2 -368.57,-366.64 2.1,-0.38 6.05,0.3 11.68,2.22 44.27,14.8 197.14,122.81 391.71,268.97 8.36,-277.381 -558.78,-766.33 -448.8,-715.678 123.75,56.95 1101.98,1070.048 1489,1473.578 54.03,56.44 96.68,101.04 123.7,129.33 11.25,12.36 22.34,25.19 33.46,37.84 5.83,825.82 -876.58,3680.24 -1370.95,3374.75" + id="path46" + style="fill:url(#radialGradient40);fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g><g + id="g48"><g + clip-path="url(#clipPath52)" + id="g50"><path + d="m 9168.6,4288.21 c -13.3,7.21 -27.81,11.88 -43.72,14.05 -430.07,56.53 -1789.26,-1741.53 -2017.92,-2132.37 -11.27,-23.43 -24.36,-54.51 -30.4,-69.89 42.91,-57.96 567.75,-752.17 920.5,-1198.891 10.31,34.77 28.86,62.981 58.37,83.231 -40.11,101.56 -48.52,192.94 19.23,238.44 -236.59,174.47 -544.27,463.57 -502.59,885.66 0.62,7.35 1.49,14.93 2.51,22.53 45.63,357.58 342.3,808.84 1138.03,1364.13 2.78,1.73 5.16,3.45 7.74,5.23 309.39,217.06 500.33,424.76 611.07,613.12 -45.42,85.99 -112.64,147.05 -162.82,174.76 z m -4284.71,563.82 c -15.89,1.99 -31.16,1.12 -45.76,-2.33 -55.66,-14.03 -136.45,-55.83 -202.17,-127.52 58.85,-210.26 190.44,-459.96 434.21,-748.67 1.98,-2.41 3.89,-4.64 5.84,-6.92 627.56,-739.83 799.26,-1251.77 752.06,-1609.13 -1.07,-7.61 -2.3,-15.21 -3.31,-22.68 -67.68,-418.52 -439.32,-619.55 -712.49,-728.18 53.97,-60.96 22.46,-147.17 -42.23,-235.16 23.21,-27.14 33.95,-59.27 35.17,-95.33 455.25,341.9 1140.58,879.32 1196.67,924.54 -1.84,16.42 -6.58,49.53 -11.41,75.06 -121.4,436.26 -976.25,2521.15 -1406.58,2576.32" + id="path62" + style="fill:url(#radialGradient56);fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></g><path + d="m 5673.29,4313.59 c -31.63,2.42 -58.64,-6.65 -81.45,-27.97 -23.18,-20.87 -35.86,-46.8 -38.15,-76.59 -2.4,-31.4 6.03,-58.4 25.66,-81.3 19.33,-23.06 44.67,-35.5 76.34,-38.18 29.76,-2.25 55.62,6.2 76.88,25.94 21.43,19.74 32.92,45.37 35.36,76.72 2.31,29.72 -4.97,57.16 -22.93,81.22 -17.73,24.61 -41.9,37.68 -71.71,40.16" + id="path64" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 576.746,3089.54 c 1.512,-64.26 11.621,-136.47 30.094,-216.65 18.394,-80.33 48.465,-157.73 90.223,-232.35 41.675,-74.67 97.074,-140.5 166.191,-197.4 68.973,-57.04 154.846,-93.56 257.576,-109.59 96.32,-14.38 182.07,-12.87 257.56,4.83 75.43,17.57 141.29,46.5 197.35,86.71 56.29,40.03 103.54,87.82 142.17,143.24 38.42,55.39 70.13,113.51 95.04,174.46 24.85,60.97 42.97,122 54.11,183.03 11.26,61.01 17.65,116.35 19.27,166.09 1.56,123.59 -14.05,232.31 -46.9,326.19 -32.93,93.94 -77.87,173.33 -134.83,238.36 -56.96,65 -123.62,115.59 -199.81,151.71 -76.23,36.04 -157.69,58.13 -244.45,66.19 -89.76,0 -176.51,-16.51 -259.87,-49.36 -83.458,-32.91 -156.974,-82.28 -220.236,-148.05 -63.492,-65.82 -114.058,-147.7 -151.734,-245.6 -37.687,-97.86 -55.066,-211.81 -51.754,-341.81 z m -175.824,4.79 c 0,134.78 19.281,257.18 57.848,367.13 38.503,109.96 92.199,204.21 161.293,282.89 68.906,78.66 150.855,140.38 245.574,185.37 94.558,44.93 197.343,69.79 308.073,74.61 125.26,9.64 241.55,-4.82 349.14,-43.3 107.55,-38.54 201.02,-97.5 280.45,-176.96 79.5,-79.43 141.62,-177.35 186.64,-293.73 44.86,-116.37 67.33,-247.58 67.33,-393.6 0,-134.84 -20.03,-257.21 -60.14,-367.12 -40.19,-109.97 -95.51,-204.33 -166.12,-282.89 -70.6,-78.67 -154.11,-140.04 -250.44,-184.18 -96.27,-44.19 -200.62,-66.93 -312.9,-68.58 -122.01,-8 -235.58,7.61 -340.74,46.87 -105.078,39.35 -196.532,98.77 -274.325,178.27 -77.937,79.42 -139.242,176.09 -184.191,290.01 -45.012,113.91 -67.492,242.33 -67.492,385.21" + id="path66" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 2813.15,3320.63 c -65.79,0 -121.6,-16.09 -167.28,-48.14 -45.68,-32.13 -82.69,-73.05 -110.72,-122.81 -28.12,-49.74 -48.14,-104.71 -60.22,-164.9 -12.02,-60.15 -17.99,-117.61 -17.99,-172.14 0,-54.57 5.57,-111.52 16.76,-170.94 11.27,-59.4 30.97,-113.98 58.94,-163.69 28.16,-49.75 65.11,-90.69 110.85,-122.72 45.69,-32.17 102.25,-48.18 169.66,-48.18 70.68,0 128.85,15.18 174.55,45.72 45.75,30.5 82.29,70.22 109.63,119.23 27.23,48.87 46.03,103.46 56.55,163.69 10.32,60.14 15.56,119.12 15.56,176.89 0,57.79 -5.24,116.77 -15.56,176.97 -10.52,60.2 -29.32,114.75 -56.55,163.7 -27.34,48.96 -63.88,89.06 -109.63,120.36 -45.7,31.31 -103.87,46.96 -174.55,46.96 z M 2127,1719.65 c 24.1,3.28 51.38,7.66 81.88,13.24 30.48,5.7 54.64,21.31 72.3,47 l 0,1533.55 c -17.66,25.59 -41.01,41.23 -69.93,46.9 -28.86,5.63 -55.33,10.03 -79.43,13.25 l 0,65 325.12,0 0,-214.26 4.81,0 c 40.13,77.04 90.98,133.65 152.83,169.75 61.71,36.05 136.05,54.18 222.67,54.18 81.88,0 154.43,-16.49 217.85,-49.39 63.41,-32.9 116.42,-77.84 158.94,-134.82 42.48,-56.96 74.67,-124.37 96.34,-202.21 21.57,-77.86 32.45,-160.87 32.45,-249.2 0,-91.43 -9.65,-175.76 -28.87,-252.74 -19.28,-77.06 -49.36,-143.66 -90.28,-199.85 -40.94,-56.21 -93.55,-100.36 -157.78,-132.4 -64.16,-32.03 -140.4,-48.2 -228.65,-48.2 -59.34,0 -111.13,9.64 -155.27,28.93 -44.12,19.27 -81.79,41.67 -113.09,67.4 -31.29,25.68 -55.87,51.81 -73.47,78.28 -17.64,26.47 -28.91,46.85 -33.67,61.38 l -4.81,0 0,-751.14 -329.94,0 0,55.35" + id="path68" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 3985.67,3337.47 c -54.6,0 -103.55,-12.44 -146.86,-37.29 -43.31,-24.89 -79.9,-56.98 -109.57,-96.32 -29.66,-39.34 -52.55,-83.45 -68.57,-132.37 -16.09,-49 -24.17,-97.54 -24.17,-145.67 l 652.54,0 c 0,51.32 -5.68,101.91 -16.9,151.67 -11.2,49.71 -28.92,93.88 -52.95,132.38 -24.1,38.56 -55.4,69.37 -93.89,92.73 -38.54,23.22 -85.05,34.87 -139.63,34.87 z m 329.85,-1088.16 c -27.35,-9.64 -62.66,-22.88 -105.91,-39.71 -43.43,-16.85 -100.4,-25.28 -171.01,-25.28 -89.89,0 -171.34,12.85 -244.32,38.53 -73.05,25.66 -134.83,65.38 -185.41,119.12 -50.57,53.77 -89.41,121.94 -116.7,204.61 -27.37,82.68 -40.13,181.01 -38.64,294.94 1.65,91.5 16.51,174.95 44.62,250.39 28.1,75.41 65.84,140.85 113.17,196.25 47.25,55.35 103.52,98.21 168.56,128.76 64.91,30.46 135.16,45.77 210.66,45.77 83.37,0 155.59,-15.64 216.64,-46.96 60.95,-31.27 110.78,-75.84 149.21,-133.63 38.57,-57.77 66.6,-127.2 84.26,-208.21 17.64,-81.11 25.71,-172.16 24.1,-273.25 l -830.64,0 c -4.79,-101.16 7.69,-188.61 37.41,-262.47 29.69,-73.8 74.21,-131.15 133.62,-172.1 59.26,-40.93 131.98,-65.43 217.84,-73.47 85.83,-8.06 183.39,2.39 292.54,31.32 l 0,-74.61" + id="path70" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 5601.06,2328.83 c 17.57,-25.82 41.67,-41.43 72.23,-47.01 30.43,-5.63 57.72,-10.05 81.82,-13.25 l 0,-60.19 -329.81,0 0,739.1 c 0,115.55 -22.07,207.02 -66.25,274.44 -44.14,67.42 -117.59,101.08 -220.24,101.08 -68.97,0 -126.48,-14.79 -172.09,-44.5 -45.76,-29.7 -82.77,-67 -110.81,-111.94 -28.03,-44.96 -47.78,-95.07 -58.98,-150.51 -11.29,-55.38 -16.86,-107.88 -16.86,-157.63 l 0,-647.6 -173.3,0 0,1097.76 c -17.73,25.66 -41.76,41.31 -72.31,46.97 -30.47,5.6 -57.76,9.99 -81.8,13.24 l 0,69.8 325.05,0 0,-226.28 c 11.2,27.29 27.71,54.96 49.28,83.02 21.74,28.09 48.96,53.81 81.81,77.05 32.93,23.26 72.3,42.13 118.06,56.56 45.75,14.47 99.05,21.69 160.08,21.69 150.85,0 257.57,-34.52 320.18,-103.53 62.58,-69.04 93.94,-170.94 93.94,-305.74 l 0,-712.53" + id="path72" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 8309.74,3438.59 0,-1230.21 -175.75,0 0,1105.06 c -17.64,25.59 -41.67,41.23 -72.22,46.9 -30.51,5.63 -57.72,10.03 -81.83,13.25 l 0,65 329.8,0" + id="path74" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 9195.63,2523.79 c -22.48,-77.06 -60.55,-139.31 -114.34,-186.57 -53.81,-47.38 -115.13,-81.54 -184.18,-102.37 -68.97,-20.84 -140.45,-28.51 -214.25,-22.88 -73.8,5.64 -141.21,22.07 -202.18,49.36 l 0,77.05 c 36.86,-19.21 85.48,-31.69 145.62,-37.28 60.16,-5.7 119.14,-1.22 176.99,13.18 57.74,14.55 107.48,40.2 149.24,77.13 41.67,36.85 62.52,88.19 62.52,153.97 0,48.19 -14.39,87.51 -43.31,117.98 -28.81,30.55 -65.31,56.63 -109.51,78.28 -44.13,21.67 -91.86,42.17 -143.26,61.37 -51.38,19.27 -99.11,42.15 -143.24,68.64 -44.11,26.47 -80.63,58.14 -109.5,95.11 -28.85,36.9 -43.37,85 -43.37,144.44 0,59.33 10.44,111.53 31.37,156.43 20.82,44.95 49.28,81.89 85.39,110.8 36.11,28.91 78.29,50.51 126.4,64.98 48.14,14.41 99.48,21.69 154.07,21.69 73.76,0 132.44,-4.04 175.8,-12.06 43.24,-8.07 79.44,-17.69 108.29,-28.87 l -2.38,-72.2 c -62.67,25.62 -124.04,40.06 -184.17,43.29 -60.23,3.16 -113.99,-5.22 -161.3,-25.29 -47.38,-20.08 -85.48,-51.74 -114.33,-95.08 -28.98,-43.31 -43.37,-97.92 -43.37,-163.69 0,-36.94 15.73,-67.47 46.96,-91.49 31.3,-24.09 70.21,-46.16 116.71,-66.22 46.55,-20.1 96.32,-40.57 149.35,-61.34 52.89,-20.91 101.42,-46.99 145.64,-78.33 44.11,-31.23 79.75,-69.81 107.11,-115.47 27.23,-45.85 37.68,-103.98 31.23,-174.56" + id="path76" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 10305.5,3563.77 c 0,40.08 -7.2,79.43 -21.6,117.95 -14.4,38.56 -35.8,72.61 -63.8,102.35 -28.1,29.66 -63,53.32 -104.8,71 -41.7,17.67 -89,26.52 -142.01,26.52 l -380.3,0 0,-674.09 351.45,0 c 51.32,0 99.06,9.18 143.16,27.68 44.2,18.45 82.2,43.7 114.4,75.81 32.1,32.07 57.4,69.86 75.8,113.18 18.5,43.33 27.7,89.85 27.7,139.6 z m -712.51,-529.6 0,-828.18 -175.78,0 0,1689.98 c -19.28,22.48 -43.44,36.53 -72.23,42.17 -28.91,5.6 -54.63,10.02 -77.05,13.2 l 0,65.04 741.47,0 c 73.8,0 139.6,-12.42 197.4,-37.33 57.9,-24.89 106.8,-58.18 146.9,-99.87 40.2,-41.79 70.6,-90.7 91.4,-146.87 21,-56.19 31.4,-116.41 31.4,-180.61 0,-60.96 -10.4,-120.77 -31.4,-179.32 -20.8,-58.61 -49.7,-111.94 -86.6,-160.07 -36.9,-48.16 -81.8,-88.28 -134.8,-120.4 -53,-32.11 -111.6,-51.36 -175.6,-57.74 l -455.11,0" + id="path78" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path + d="m 8241.12,3976.16 c -31.65,2.42 -58.72,-6.75 -81.68,-28.03 -23.08,-20.82 -35.63,-46.81 -38,-76.59 -2.47,-31.36 5.96,-58.4 25.52,-81.33 19.36,-23.02 44.88,-35.59 76.43,-38.15 29.74,-2.21 55.6,6.17 76.8,25.94 21.43,19.75 33.14,45.35 35.49,76.7 2.39,29.66 -4.96,57.16 -22.96,81.22 -17.85,24.66 -41.94,37.74 -71.6,40.24" + id="path80" + style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></svg> \ No newline at end of file diff --git a/openwisp_utils/admin_theme/templates/admin/base_site.html b/openwisp_utils/admin_theme/templates/admin/base_site.html new file mode 100644 index 00000000..e95ec4db --- /dev/null +++ b/openwisp_utils/admin_theme/templates/admin/base_site.html @@ -0,0 +1,15 @@ +{% extends "admin/base.html" %} +{% load static %} + +{% block title %}{{ title }} | {{ site_title }}{% endblock %} + +{% block extrastyle %} +<link rel="stylesheet" type="text/css" href="{% static "admin/css/openwisp.css" %}" /> +<link rel="icon" type="image/x-icon" href="{% static "ui/openwisp/images/favicon.png" %}" /> +{% endblock %} + +{% block branding %} +<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header }}</a></h1> +{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/openwisp_utils/admin_theme/templates/base.html b/openwisp_utils/admin_theme/templates/base.html new file mode 100644 index 00000000..7e2012df --- /dev/null +++ b/openwisp_utils/admin_theme/templates/base.html @@ -0,0 +1,2 @@ +{% extends 'admin/base_site.html' %} +{% block title %}OpenWISP2{% endblock %} diff --git a/openwisp_utils/base.py b/openwisp_utils/base.py new file mode 100644 index 00000000..5d74db67 --- /dev/null +++ b/openwisp_utils/base.py @@ -0,0 +1,18 @@ +import uuid + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from model_utils.fields import AutoCreatedField, AutoLastModifiedField + + +class TimeStampedEditableModel(models.Model): + """ + An abstract base class model that provides self-updating + ``created`` and ``modified`` fields. + """ + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + created = AutoCreatedField(_('created'), editable=True) + modified = AutoLastModifiedField(_('modified'), editable=True) + + class Meta: + abstract = True diff --git a/openwisp_utils/loaders.py b/openwisp_utils/loaders.py new file mode 100644 index 00000000..6146b25b --- /dev/null +++ b/openwisp_utils/loaders.py @@ -0,0 +1,20 @@ +import os + +from django.template.loaders.filesystem import Loader as FilesystemLoader + +from .settings import EXTENDED_APPS + + +class DependencyLoader(FilesystemLoader): + """ + A template loader that looks in templates dir of + django-apps listed in dependencies. Default values is [] + """ + dependencies = EXTENDED_APPS + + def get_dirs(self): + dirs = [] + for dependency in self.dependencies: + module = __import__(dependency) + dirs.append('{0}/templates'.format(os.path.dirname(module.__file__))) + return dirs diff --git a/openwisp_utils/settings.py b/openwisp_utils/settings.py new file mode 100644 index 00000000..ec26f2d7 --- /dev/null +++ b/openwisp_utils/settings.py @@ -0,0 +1,4 @@ +from django.conf import settings + + +EXTENDED_APPS = getattr(settings, 'EXTENDED_APPS', []) diff --git a/openwisp_utils/staticfiles.py b/openwisp_utils/staticfiles.py new file mode 100644 index 00000000..10390910 --- /dev/null +++ b/openwisp_utils/staticfiles.py @@ -0,0 +1,28 @@ +import collections +import os + +from django.contrib.staticfiles.finders import FileSystemFinder +from django.core.files.storage import FileSystemStorage + +from .settings import EXTENDED_APPS + + +class DependencyFinder(FileSystemFinder): + """ + A static files finder that finds static files of + django-apps listed in dependencies + """ + dependencies = EXTENDED_APPS + + def __init__(self, app_names=None, *args, **kwargs): + self.locations = [] + self.storages = collections.OrderedDict() + for dependency in self.dependencies: + module = __import__(dependency) + path = '{0}/static'.format(os.path.dirname(module.__file__)) + self.locations.append(('', path)) + for prefix, root in self.locations: + filesystem_storage = FileSystemStorage(location=root) + filesystem_storage.prefix = prefix + self.storages[root] = filesystem_storage + super(FileSystemFinder, self).__init__(*args, **kwargs) diff --git a/openwisp_utils/tests/__init__.py b/openwisp_utils/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openwisp_utils/tests/utils.py b/openwisp_utils/tests/utils.py new file mode 100644 index 00000000..226ea419 --- /dev/null +++ b/openwisp_utils/tests/utils.py @@ -0,0 +1,87 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.db.models import Q +from django.urls import reverse + +from openwisp_users.models import OrganizationUser + +user_model = get_user_model() + + +class TestMultitenantAdminMixin(object): + def setUp(self): + user_model.objects.create_superuser(username='admin', + password='tester', + email='admin@admin.com') + + def _login(self, username='admin', password='tester'): + self.client.login(username=username, password=password) + + def _logout(self): + self.client.logout() + + operator_permission_filters = [] + + def get_operator_permissions(self): + filters = Q() + for filter in self.operator_permission_filters: + filters = filters | Q(**filter) + return Permission.objects.filter(filters) + + def _create_operator(self, organizations=[]): + operator = user_model.objects.create_user(username='operator', + password='tester', + email='operator@test.com', + is_staff=True) + operator.user_permissions.add(*self.get_operator_permissions()) + for organization in organizations: + OrganizationUser.objects.create(user=operator, organization=organization) + return operator + + def _test_multitenant_admin(self, url, visible, hidden, select_widget=False): + """ + reusable test function that ensures different users + can see the right objects. + an operator with limited permissions will not be able + to see the elements contained in ``hidden``, while + a superuser can see everything. + """ + self._login(username='operator', password='tester') + response = self.client.get(url) + + # utility format function + def _f(el, select_widget=False): + if select_widget: + return '{0}</option>'.format(el) + return el + + # ensure elements in visible list are visible to operator + for el in visible: + self.assertContains(response, _f(el, select_widget), + msg_prefix='[operator contains]') + # ensure elements in hidden list are not visible to operator + for el in hidden: + self.assertNotContains(response, _f(el, select_widget), + msg_prefix='[operator not-contains]') + + # now become superuser + self._logout() + self._login(username='admin', password='tester') + response = self.client.get(url) + # ensure all elements are visible to superuser + all_elements = visible + hidden + for el in all_elements: + self.assertContains(response, _f(el, select_widget), + msg_prefix='[superuser contains]') + + def _test_changelist_recover_deleted(self, app_label, model_label): + self._test_multitenant_admin( + url=reverse('admin:{0}_{1}_changelist'.format(app_label, model_label)), + visible=[], + hidden=['Recover deleted'] + ) + + def _test_recoverlist_operator_403(self, app_label, model_label): + self._login(username='operator', password='tester') + response = self.client.get(reverse('admin:{0}_{1}_recoverlist'.format(app_label, model_label))) + self.assertEqual(response.status_code, 403) diff --git a/requirements-test.txt b/requirements-test.txt index e49e1d15..2ed2aec5 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,3 +2,5 @@ coverage coveralls isort flake8 +# For testing Dependency loaders +django_netjsonconfig diff --git a/requirements.txt b/requirements.txt index 5c1e7ec3..fe4a0276 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ +django-model-utils openwisp-users<0.2 diff --git a/runtests.py b/runtests.py index 6870ba00..226754a3 100755 --- a/runtests.py +++ b/runtests.py @@ -11,5 +11,5 @@ from django.core.management import execute_from_command_line args = sys.argv args.insert(1, "test") - args.insert(2, "openwisp_utils") -execute_from_command_line(args) + args.insert(2, "test_project") + execute_from_command_line(args) diff --git a/tests/settings.py b/tests/settings.py index 84b54d36..c8e1210d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -8,16 +8,29 @@ ALLOWED_HOSTS = [] - INSTALLED_APPS = [ - 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'openwisp_utils.admin_theme', + # all-auth + 'django.contrib.sites', + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'django_extensions', + # openwisp2 modules + 'openwisp_users', + # test project + 'test_project', + # admin + 'django.contrib.admin', ] +EXTENDED_APPS = ['django_netjsonconfig'] # Just for testing purposes + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -41,8 +54,12 @@ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], - 'APP_DIRS': True, 'OPTIONS': { + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + 'openwisp_utils.loaders.DependencyLoader', + ], 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', @@ -60,3 +77,14 @@ } } +AUTH_USER_MODEL = 'openwisp_users.User' +SITE_ID = '1' +EMAIL_PORT = '1025' +LOGIN_REDIRECT_URL = 'admin:index' +ACCOUNT_LOGOUT_REDIRECT_URL = LOGIN_REDIRECT_URL + +# local settings must be imported before test runner otherwise they'll be ignored +try: + from local_settings import * +except ImportError: + pass diff --git a/tests/test_project/__init__.py b/tests/test_project/__init__.py new file mode 100644 index 00000000..5ace57c9 --- /dev/null +++ b/tests/test_project/__init__.py @@ -0,0 +1 @@ +default_app_config = 'test_project.apps.TestAppConfig' diff --git a/tests/test_project/admin.py b/tests/test_project/admin.py new file mode 100644 index 00000000..7f193434 --- /dev/null +++ b/tests/test_project/admin.py @@ -0,0 +1,30 @@ +from django.contrib import admin + +from openwisp_utils.admin import (MultitenantAdminMixin, + MultitenantObjectFilter, + MultitenantOrgFilter, + TimeReadonlyAdminMixin) + +from .models import Book, Shelf + + +class BaseAdmin(MultitenantAdminMixin, TimeReadonlyAdminMixin, admin.ModelAdmin): + pass + + +class ShelfAdmin(BaseAdmin): + list_display = ['name', 'organization'] + list_filter = [('organization', MultitenantOrgFilter)] + fields = ['name', 'organization', 'created', 'modified'] + + +class BookAdmin(BaseAdmin): + list_display = ['name', 'author', 'organization', 'shelf'] + list_filter = [('organization', MultitenantOrgFilter), + ('shelf', MultitenantObjectFilter)] + fields = ['name', 'author', 'organization', 'shelf', 'created', 'modified'] + multitenant_shared_relations = ['shelf'] + + +admin.site.register(Shelf, ShelfAdmin) +admin.site.register(Book, BookAdmin) diff --git a/tests/test_project/apps.py b/tests/test_project/apps.py new file mode 100644 index 00000000..faff0087 --- /dev/null +++ b/tests/test_project/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TestAppConfig(AppConfig): + name = 'test_project' + label = 'test_project' diff --git a/tests/test_project/migrations/0001_initial.py b/tests/test_project/migrations/0001_initial.py new file mode 100644 index 00000000..f1c97aef --- /dev/null +++ b/tests/test_project/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-06-23 14:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('openwisp_users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=64, verbose_name='name')), + ('author', models.CharField(max_length=64, verbose_name='author')), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='openwisp_users.Organization', verbose_name='organization')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Shelf', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=64, verbose_name='name')), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='openwisp_users.Organization', verbose_name='organization')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='book', + name='shelf', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='test_project.Shelf'), + ), + ] diff --git a/tests/test_project/migrations/__init__.py b/tests/test_project/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_project/models.py b/tests/test_project/models.py new file mode 100644 index 00000000..8b0415c7 --- /dev/null +++ b/tests/test_project/models.py @@ -0,0 +1,27 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from openwisp_users.mixins import OrgMixin +from openwisp_utils.base import TimeStampedEditableModel + + +class Shelf(OrgMixin, TimeStampedEditableModel): + name = models.CharField(_('name'), max_length=64) + + def __str__(self): + return self.name + + class Meta: + abstract = False + + +class Book(OrgMixin, TimeStampedEditableModel): + name = models.CharField(_('name'), max_length=64) + author = models.CharField(_('author'), max_length=64) + shelf = models.ForeignKey('test_project.Shelf') + + def __str__(self): + return self.name + + class Meta: + abstract = False diff --git a/tests/test_project/tests/__init__.py b/tests/test_project/tests/__init__.py new file mode 100644 index 00000000..87258ebb --- /dev/null +++ b/tests/test_project/tests/__init__.py @@ -0,0 +1,17 @@ +class CreateMixin(object): + def _create_book(self, **kwargs): + options = dict(name='test-book', + author='test-author') + options.update(kwargs) + b = self.book_model(**options) + b.full_clean() + b.save() + return b + + def _create_shelf(self, **kwargs): + options = dict(name='test-shelf') + options.update(kwargs) + s = self.shelf_model(**options) + s.full_clean() + s.save() + return s diff --git a/tests/test_project/tests/test_admin.py b/tests/test_project/tests/test_admin.py new file mode 100644 index 00000000..d7dc4114 --- /dev/null +++ b/tests/test_project/tests/test_admin.py @@ -0,0 +1,90 @@ +from django.test import TestCase +from django.urls import reverse + +from openwisp_users.tests.utils import TestOrganizationMixin +from openwisp_utils.tests.utils import TestMultitenantAdminMixin + +from . import CreateMixin +from ..models import Book, Shelf + + +class TestAdmin(CreateMixin, TestMultitenantAdminMixin, + TestOrganizationMixin, TestCase): + book_model = Book + shelf_model = Shelf + operator_permission_filter = [ + {'codename__endswith': 'book'}, + {'codename__endswith': 'shelf'}, + ] + + def _create_multitenancy_test_env(self): + org1 = self._create_org(name='org1') + org2 = self._create_org(name='org2') + inactive = self._create_org(name='inactive-org', is_active=False) + operator = self._create_operator(organizations=[org1, inactive]) + s1 = self._create_shelf(name='shell1', organization=org1) + s2 = self._create_shelf(name='shell2', organization=org2) + s3 = self._create_shelf(name='shell3', organization=inactive) + b1 = self._create_book(name='book1', organization=org1, shelf=s1) + b2 = self._create_book(name='book2', organization=org2, shelf=s2) + b3 = self._create_book(name='book3', organization=inactive, shelf=s3) + data = dict(s1=s1, s2=s2, s3_inactive=s3, + b1=b1, b2=b2, b3_inactive=b3, + org1=org1, org2=org2, + inactive=inactive, + operator=operator) + return data + + def test_shelf_queryset(self): + data = self._create_multitenancy_test_env() + self._test_multitenant_admin( + url=reverse('admin:test_project_shelf_changelist'), + visible=[data['s1'].name, data['org1'].name], + hidden=[data['s2'].name, data['org2'].name, + data['s3_inactive'].name] + ) + + def test_shelf_organization_fk_queryset(self): + data = self._create_multitenancy_test_env() + self._test_multitenant_admin( + url=reverse('admin:test_project_shelf_add'), + visible=[data['org1'].name], + hidden=[data['org2'].name, data['inactive']], + select_widget=True + ) + + def test_book_queryset(self): + data = self._create_multitenancy_test_env() + self._test_multitenant_admin( + url=reverse('admin:test_project_book_changelist'), + visible=[data['b1'].name, data['org1'].name], + hidden=[data['b2'].name, data['org2'].name, + data['b3_inactive'].name] + ) + + def test_book_organization_fk_queryset(self): + data = self._create_multitenancy_test_env() + self._test_multitenant_admin( + url=reverse('admin:test_project_book_add'), + visible=[data['org1'].name], + hidden=[data['org2'].name, data['inactive']], + select_widget=True + ) + + def test_book_shelf_filter(self): + data = self._create_multitenancy_test_env() + s_special = self._create_shelf(name='special', organization=data['org1']) + self._test_multitenant_admin( + url=reverse('admin:test_project_book_changelist'), + visible=[data['s1'].name, s_special.name], + hidden=[data['s2'].name, data['s3_inactive'].name] + ) + + def test_book_shelf_fk_queryset(self): + data = self._create_multitenancy_test_env() + self._test_multitenant_admin( + url=reverse('admin:test_project_book_add'), + visible=[data['s1'].name], + hidden=[data['s2'].name, data['s3_inactive'].name], + select_widget=True + ) diff --git a/tests/test_project/tests/test_staticfinders.py b/tests/test_project/tests/test_staticfinders.py new file mode 100644 index 00000000..983fc9f2 --- /dev/null +++ b/tests/test_project/tests/test_staticfinders.py @@ -0,0 +1,10 @@ +import unittest + +from openwisp_utils.staticfiles import DependencyFinder + + +class TestStaticFinders(unittest.TestCase): + def test_dependency_finder(self): + finder = DependencyFinder() + self.assertIsInstance(finder.locations, list) + self.assertIn('django_netjsonconfig', finder.locations[0][1]) diff --git a/tests/urls.py b/tests/urls.py index a9bad669..b230065e 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,21 +1,10 @@ -"""testapp URL Configuration +from django.conf.urls import include, url -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.11/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.conf.urls import url, include - 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) -""" -from django.conf.urls import url -from django.contrib import admin +from openwisp_utils.admin_theme.admin import admin, openwisp_admin + +openwisp_admin() urlpatterns = [ - url(r'^admin/', admin.site.urls), + url(r'^accounts/', include('openwisp_users.accounts.urls')), + url(r'^admin/', include(admin.site.urls)), ]