Rafał Pitoń 10 лет назад
Родитель
Сommit
c852111a6e

+ 7 - 0
misago/emberapp/app/components/pagination-aligned.js

@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  tagName: 'nav',
+
+  classNames: ['pagination-aligned']
+});

+ 14 - 0
misago/emberapp/app/components/user-card.js

@@ -0,0 +1,14 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  classNames: ['user-card'],
+  classNameBindings: ['rankClass'],
+
+  rankClass: function() {
+    if (this.get('user.rank.css_class').length) {
+      return 'user-card-' + this.get('user.rank.css_class');
+    } else {
+      return ''
+    }
+  }.property('user.rank.css_class')
+});

+ 3 - 1
misago/emberapp/app/components/user-state-icon.js

@@ -1,3 +1,5 @@
 import UserStateLabel from 'misago/components/user-state-label';
 
-export default UserStateLabel.extend();
+export default UserStateLabel.extend({
+  tagName: 'span'
+});

+ 21 - 0
misago/emberapp/app/mixins/exceptions-shortcuts.js

@@ -0,0 +1,21 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+  // Shorthands for raising errors
+  throw403: function(reason) {
+    if (reason) {
+      throw {
+        status: 403,
+        responseJSON: {
+          detail: reason
+        }
+      };
+    } else {
+      throw { status: 403 };
+    }
+  },
+
+  throw404: function() {
+    throw { status: 404 };
+  }
+});

+ 21 - 0
misago/emberapp/app/mixins/model-pagination.js

@@ -0,0 +1,21 @@
+import Ember from 'ember';
+
+export default Ember.Mixin.create({
+  // Shorthand for validating page number
+  cleanPage: function(page, transition) {
+    var cleanPage = parseInt(page);
+    if ("" + cleanPage === page && cleanPage > 0) {
+      if (cleanPage > 1) {
+        // return page number for an app
+        return cleanPage;
+      } else {
+        // redirect to first page
+        var routePath = transition.targetName.split('.');
+        routePath[routePath.length - 1] = 'index';
+        this.transitionTo(routePath.join('.'));
+      }
+    } else {
+      this.throw404(); // not a valid page number
+    }
+  },
+});

+ 10 - 38
misago/emberapp/app/routes/misago.js

@@ -1,42 +1,14 @@
 import Ember from 'ember';
 import DocumentTitle from 'misago/mixins/document-title';
-import ResetScroll from 'misago/mixins/reset-scroll';
+import ExceptionsShortcuts from 'misago/mixins/exceptions-shortcuts';
+import ModelPagination from 'misago/mixins/model-pagination';
 import ModelUrlName from 'misago/mixins/model-url-name';
+import ResetScroll from 'misago/mixins/reset-scroll';
 
-export default Ember.Route.extend(DocumentTitle, ResetScroll, ModelUrlName, {
-  // Shorthand for validating page number
-  cleanPage: function(page, transition) {
-    var cleanPage = parseInt(page);
-    if ("" + cleanPage === page && cleanPage > 0) {
-      if (cleanPage > 1) {
-        // return page number for an app
-        return cleanPage;
-      } else {
-        // redirect to first page
-        var routePath = transition.targetName.split('.');
-        routePath[routePath.length - 1] = 'index';
-        this.transitionTo(routePath.join('.'));
-      }
-    } else {
-      this.throw404(); // not a valid page number
-    }
-  },
-
-  // Shorthands for raising errors
-  throw403: function(reason) {
-    if (reason) {
-      throw {
-        status: 403,
-        responseJSON: {
-          detail: reason
-        }
-      };
-    } else {
-      throw { status: 403 };
-    }
-  },
-
-  throw404: function() {
-    throw { status: 404 };
-  }
-});
+export default Ember.Route.extend(
+  DocumentTitle,
+  ExceptionsShortcuts,
+  ModelPagination,
+  ModelUrlName,
+  ResetScroll
+);

+ 0 - 1
misago/emberapp/app/routes/user.js

@@ -4,7 +4,6 @@ export default MisagoRoute.extend({
   usingUrlName: true,
 
   model: function(params) {
-    console.log(params);
     var urlName = this.getParsedUrlNameOr404(params.url_name);
     return this.store.find('user-profile', urlName.id);
   },

+ 1 - 1
misago/emberapp/app/routes/users/rank/index.js

@@ -13,7 +13,7 @@ export default MisagoRoute.extend({
     this.controllerFor('users.rank').setProperties({
       'rank': this.modelFor('users.rank'),
       'model': model,
-      'meta': model.get("meta")
+      'meta': model.get('meta')
     });
   },
 

+ 2 - 11
misago/emberapp/app/routes/users/rank/page.js

@@ -1,6 +1,6 @@
-import MisagoRoute from 'misago/routes/misago';
+import IndexRoute from 'misago/routes/users/rank/index';
 
-export default MisagoRoute.extend({
+export default IndexRoute.extend({
   page: 0,
 
   model: function(params, transition) {
@@ -16,15 +16,6 @@ export default MisagoRoute.extend({
     }
   },
 
-  templateName: 'users/rank',
-  setupController: function(controller, model) {
-    this.controllerFor('users.rank').setProperties({
-      'rank': this.modelFor('users.rank'),
-      'model': model,
-      'meta': model.get("content.meta")
-    });
-  },
-
   actions: {
     didTransition: function() {
       this.set('title', {

+ 19 - 0
misago/emberapp/app/serializers/application.js

@@ -0,0 +1,19 @@
+import DRFSerializer from './drf';
+
+export default DRFSerializer.extend({
+  // Custom meta-data handling that's not discarding extra metadata
+  // from api endpoints.
+  extractMeta: function(store, type, payload) {
+    if (payload && payload.results) {
+      var meta = {};
+      for(var k in payload) {
+        if (k !== 'results') {
+          meta[k] = payload[k];
+        }
+      }
+
+      // Pass metadata to Ember store
+      store.setMetadataFor(type, meta);
+    }
+  }
+});

+ 1 - 0
misago/emberapp/app/styles/misago/misago.less

@@ -15,6 +15,7 @@
 @import "typo.less";
 @import "misc.less";
 @import "ui-preview.less";
+@import "user-card.less";
 
 // Pages
 @import "errorpages.less";

+ 102 - 0
misago/emberapp/app/styles/misago/user-card.less

@@ -0,0 +1,102 @@
+//
+// User Profile Card
+// --------------------------------------------------
+
+
+// Shape card
+.user-card {
+  border-radius: @user-card-border-radius;
+  margin: @line-height-computed / 2 0px;
+  overflow: hidden;
+  position: relative;
+
+  .media {
+    margin: 0px;
+  }
+}
+
+
+// Background avatar
+.user-card>.user-avatar {
+  width: 100%;
+  height: auto;
+}
+
+
+// Avatar overlay
+.user-card .avatar-overlay {
+  background: @user-card-overlay;
+
+  position: absolute;
+  top: 0%;
+  left: 0%;
+
+  width: 100%;
+  height: 100%;
+  min-height: 100%;
+
+  .avatar-overlay-cover {
+    display: table;
+    width: 100%;
+    height: 100%;
+    min-height: 100%;
+
+    .avatar-overlay-inner {
+      display: table-cell;
+      padding: @padding-large-vertical @padding-large-horizontal;
+
+      text-align: center;
+      vertical-align: middle;
+    }
+  }
+}
+
+
+// Card user avatar link
+.user-card .user-avatar-link {
+  background-color: @user-card-avatar-link;
+  border-radius: @avatar-radius;
+  display: block;
+  margin: 0px auto;
+  overflow: hidden;
+  width: 60%;
+
+  img {
+    width: 100%;
+    height: auto;
+  }
+
+  &:link, &:visited {
+    border: 4px solid @user-card-avatar-link;
+  }
+
+  &:hover, &:focus {
+    border-color: @user-card-avatar-link-hover;
+  }
+
+  &:active {
+    border-color: @user-card-avatar-link-active;
+  }
+}
+
+// Card nav
+.user-card nav a {
+  margin: 0px 4px;
+
+  .material-icons {
+    font-size: @font-size-base * 1.5;
+  }
+
+  &:link, &:visited {
+    color: @site-link-color;
+  }
+
+  &:hover, &:focus {
+    color: @site-link-hover-color;
+    text-decoration: none;
+  }
+
+  &:active {
+    color: @site-link-active-color;
+  }
+}

+ 11 - 0
misago/emberapp/app/styles/misago/variables.less

@@ -324,3 +324,14 @@
 //** Animation
 @ui-preview-bg:                   @gray-lighter;
 @ui-preview-light:                darken(@ui-preview-bg, 10%);
+
+
+//== User Card
+//
+//** Body
+@user-card-border-radius:         @border-radius-large;
+@user-card-overlay:               fadeOut(@body-bg, 10%);
+
+@user-card-avatar-link:           fadeOut(#fff, 50%);
+@user-card-avatar-link-hover:     @link-color;
+@user-card-avatar-link-active:    @link-hover-color;

+ 33 - 0
misago/emberapp/app/templates/components/pagination-aligned.hbs

@@ -0,0 +1,33 @@
+<ul class="pager">
+  {{#if meta.first}}
+    <li class="previous">
+      {{#link-to (join-strings path ".index") model}}
+        {{gettext "Start"}}
+      {{/link-to}}
+    </li>
+  {{/if}}
+
+  {{#if meta.previous}}
+    <li class="previous">
+      {{#link-to (join-strings path ".page") model meta.previous}}
+        {{gettext "Previous"}}
+      {{/link-to}}
+    </li>
+  {{/if}}
+
+  {{#if meta.last}}
+    <li class="next">
+      {{#link-to (join-strings path ".page") model meta.last}}
+        {{gettext "End"}}
+      {{/link-to}}
+    </li>
+  {{/if}}
+
+  {{#if meta.next}}
+    <li class="next">
+      {{#link-to (join-strings path ".page") model meta.next}}
+        {{gettext "Next"}}
+      {{/link-to}}
+    </li>
+  {{/if}}
+</ul>

+ 29 - 0
misago/emberapp/app/templates/components/user-card.hbs

@@ -0,0 +1,29 @@
+{{user-avatar user=user size=400}}
+<div class="avatar-overlay">
+  <div class="avatar-overlay-cover">
+    <div class="avatar-overlay-inner">
+
+      {{#link-to 'user' user.url_name class="user-avatar-link"}}
+        {{user-avatar user=user size=150}}
+      {{/link-to}}
+
+      <h4>
+        {{user-state-icon user=user}}
+        {{user.username}}
+      </h4>
+
+      <nav>
+        {{#link-to 'user' user.url_name class="btn btn-default btn-outline"}}
+          <i class="material-icons">person_outline</i>
+        {{/link-to}}
+        {{#link-to 'user' user.url_name class="btn btn-default btn-outline"}}
+          <i class="material-icons">chat</i>
+        {{/link-to}}
+        {{#link-to 'user' user.url_name class="btn btn-default btn-outline"}}
+          <i class="material-icons">check</i>
+        {{/link-to}}
+      </nav>
+
+    </div>
+  </div>
+</div>

+ 19 - 8
misago/emberapp/app/templates/users/rank.hbs

@@ -1,11 +1,22 @@
-{{rank.name}}
+<div class="rank-page {{if rank.css_class (join-strings "rank-page-" rank.css_class)}}">
+  {{#if rank.description}}
+    <div class="rank-description lead">
+      {{{rank.description}}}
+    </div>
+  {{/if}}
 
-{{join-strings 'lorem' 'ipsum' 'dolor' 'met'}}
+  <div class="rank-users">
+    {{#each (batch-row model 4) as |row|}}
+      <div class="row">
+        {{#each row as |user|}}
+          <div class="col-md-3">
+            {{user-card user=user}}
+          </div>
+        {{/each}}
+      </div>
+    {{/each}}
+  </div>
 
-<hr>
+  {{pagination-aligned path='users.rank' model=rank meta=meta}}
 
-{{#each model as |user|}}
-  {{#link-to "user" user.url_name}}
-    {{user-avatar user=user size=200}}
-  {{/link-to}}
-{{/each}}
+</div>

+ 55 - 0
misago/emberapp/tests/acceptance/model-pagination-test.js

@@ -0,0 +1,55 @@
+import Ember from 'ember';
+import { module, test } from 'qunit';
+import startApp from '../helpers/start-app';
+import createUser from '../helpers/create-user';
+
+var application, container, auth;
+
+module('Acceptance: Model Pagination Mixin', {
+  beforeEach: function() {
+    application = startApp();
+    container = application.__container__;
+    auth = container.lookup('service:auth');
+  },
+
+  afterEach: function() {
+    Ember.run(application, 'destroy');
+    Ember.$.mockjax.clear();
+  }
+});
+
+test('pagination redirects from explicit first page', function(assert) {
+  var user = createUser();
+  auth.setProperties({
+    'isAuthenticated': true,
+    'user': user
+  });
+
+  Ember.$.mockjax({
+    url: '/api/ranks/',
+    status: 200,
+    responseText: [{
+      'id': 3,
+      'name': 'Test Rank',
+      'slug': 'test-rank',
+      'description': '',
+      'css_class': '',
+      'is_tab': true
+    }]
+  });
+
+  Ember.$.mockjax({
+    url: '/api/users/?list=rank&rank=test-rank',
+    status: 200,
+    responseText: []
+  });
+
+  assert.expect(1);
+
+  visit('/users/');
+
+  andThen(function() {
+    console.log(find('.nav-tabs').text())
+    assert.equal(currentPath(), 'users.rank.index');
+  });
+});

+ 1 - 1
misago/emberapp/tests/acceptance/model-url-name-mixin-test.js

@@ -5,7 +5,7 @@ import ModelUrlName from 'misago/mixins/model-url-name';
 
 var application;
 
-module('Acceptance: Page Title Mixin', {
+module('Acceptance: Model Url Name', {
   beforeEach: function() {
     application = startApp();
   },

+ 0 - 10
misago/emberapp/tests/unit/utils/datetime-formats-test.js

@@ -1,10 +0,0 @@
-import datetimeFormats from '../../../utils/datetime-formats';
-import { module, test } from 'qunit';
-
-module('datetimeFormats');
-
-// Replace this with your real tests.
-test('it works', function(assert) {
-  var result = datetimeFormats();
-  assert.ok(result);
-});

+ 3 - 6
misago/users/api/usernamechanges.py

@@ -2,9 +2,10 @@ from django.core.exceptions import PermissionDenied
 from django.utils.translation import ugettext as _
 
 from rest_framework import status, viewsets, mixins
-from rest_framework.pagination import PageNumberPagination
 from rest_framework.response import Response
 
+from misago.core.apipaginator import ApiPaginator
+
 from misago.users.models import UsernameChange
 from misago.users.rest_permissions import BasePermission
 from misago.users.serializers.usernamechange import UsernameChangeSerializer
@@ -25,15 +26,11 @@ class UsernameChangesViewSetPermission(BasePermission):
         return True
 
 
-class UsernameChangesPagination(PageNumberPagination):
-    page_size = 20
-
-
 class UsernameChangesViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
     permission_classes = (UsernameChangesViewSetPermission,)
     serializer_class = UsernameChangeSerializer
     queryset = UsernameChange.objects
-    pagination_class = UsernameChangesPagination
+    pagination_class = ApiPaginator(20, 5)
 
     def get_queryset(self):
         queryset = UsernameChange.objects.select_related('user', 'changed_by')

+ 10 - 9
misago/users/api/users.py

@@ -5,11 +5,11 @@ from django.utils.translation import ugettext as _
 
 from rest_framework import mixins, status, viewsets
 from rest_framework.decorators import detail_route, list_route
-from rest_framework.pagination import PageNumberPagination
 from rest_framework.parsers import JSONParser, MultiPartParser
 from rest_framework.response import Response
 
 from misago.acl import add_acl
+from misago.core.apipaginator import ApiPaginator
 
 from misago.users.forms.options import ForumOptionsForm
 from misago.users.permissions.profiles import (allow_browse_users_list,
@@ -45,16 +45,12 @@ def allow_self_only(user, pk, message):
         raise PermissionDenied(message)
 
 
-class UsersPagination(PageNumberPagination):
-    page_size = 16
-
-
 class UserViewSet(viewsets.GenericViewSet):
     permission_classes = (UserViewSetPermission,)
     parser_classes=(JSONParser, MultiPartParser)
     serializer_class = UserSerializer
     queryset = get_user_model().objects
-    pagination_class = UsersPagination
+    pagination_class = ApiPaginator(16, 4)
 
     def get_queryset(self):
         relations = ('rank', 'online_tracker', 'ban_cache')
@@ -82,10 +78,15 @@ class UserViewSet(viewsets.GenericViewSet):
         serializer = serializer_class(
             users_list['queryset'], many=True, context={'user': request.user})
 
+        response_dict = {
+            'results': serializer.data,
+            'users': self.queryset.count()
+        }
+
         if users_list.get('paginate'):
-            return self.get_paginated_response(serializer.data)
-        else:
-            return Response(serializer.data)
+            response_dict.update(self.paginator.get_meta())
+
+        return Response(response_dict)
 
     def create(self, request):
         return create_endpoint(request)

+ 1 - 1
misago/users/serializers/ban.py

@@ -7,7 +7,7 @@ from misago.core.utils import format_plaintext_for_html
 from misago.users.models import Ban, BAN_IP
 
 
-__ALL__ = ['BanMessageSerializer']
+__all__ = ['BanMessageSerializer']
 
 
 class BanMessageSerializer(serializers.ModelSerializer):

+ 10 - 2
misago/users/serializers/rank.py

@@ -1,12 +1,14 @@
 from rest_framework import serializers
-
+from misago.core.utils import format_plaintext_for_html
 from misago.users.models import Rank
 
 
-__ALL__ = ['RankSerializer']
+__all__ = ['RankSerializer']
 
 
 class RankSerializer(serializers.ModelSerializer):
+    description = serializers.SerializerMethodField()
+
     class Meta:
         model = Rank
         fields = (
@@ -17,3 +19,9 @@ class RankSerializer(serializers.ModelSerializer):
             'title',
             'css_class',
             'is_tab')
+
+    def get_description(self, obj):
+        if obj.description:
+            return format_plaintext_for_html(obj.description)
+        else:
+            return ''

+ 1 - 1
misago/users/serializers/user.py

@@ -89,7 +89,7 @@ class UserSerializer(serializers.ModelSerializer):
         )
 
     def get_state(self, obj):
-        if hasattr(obj, 'online_tracker'):
+        if 'user' in self.context:
             return get_user_state(obj, self.context['user'].acl)
         else:
             return {}

+ 3 - 0
misago/users/serializers/usernamechange.py

@@ -4,6 +4,9 @@ from misago.users.models import UsernameChange
 from misago.users.serializers.user import BasicUserSerializer
 
 
+__all__ = ['UsernameChangeSerializer']
+
+
 class UsernameChangeSerializer(serializers.ModelSerializer):
     user = BasicUserSerializer(many=False, read_only=True)
     changed_by = BasicUserSerializer(many=False, read_only=True)