Browse Source

wip user profile

Rafał Pitoń 10 years ago
parent
commit
7f91ce1c9b

+ 1 - 1
misago/emberapp/app/components/forms/login-form.js

@@ -58,7 +58,7 @@ export default Ember.Component.extend({
   },
 
   error: function(jqXHR) {
-    self.set('isBusy', false);
+    this.set('isBusy', false);
 
     var rejection = jqXHR.responseJSON;
     if (jqXHR.status !== 400) {

+ 20 - 0
misago/emberapp/app/components/link-dropdown-toggle.js

@@ -0,0 +1,20 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+  tagName: 'a',
+
+  attributeBindings: ['href', 'toggle:data-toggle', 'expanded:aria-expanded'],
+  classNames: 'dropdown-toggle',
+  toggle: 'dropdown',
+  expanded: 'false',
+  ariaRoleString: 'button',
+
+  href: function() {
+    var router = this.container.lookup('router:main');
+
+    var route = this.get('route');
+    var params = this.get('params');
+
+    return router.generate(route, params);
+  }.property('route', 'params.@each')
+});

+ 45 - 0
misago/emberapp/app/mixins/model-url-name.js

@@ -0,0 +1,45 @@
+import Ember from 'ember';
+
+var urlNameRe = new RegExp(/^([a-zA-Z0-9-]+)-(\d+)$/);
+
+export default Ember.Mixin.create({
+  usingUrlName: false,
+
+  parseUrlName: function(urlName) {
+    if (urlNameRe.test(urlName)) {
+      var idPosition = urlName.lastIndexOf('-');
+      return {
+        slug: urlName.substr(0, idPosition),
+        id: urlName.substr(idPosition + 1)
+      };
+    } else {
+      return false;
+    }
+  },
+
+  getParsedUrlNameOr404: function(urlName) {
+    var parsedUrlName = this.parseUrlName(urlName);
+    if (parsedUrlName) {
+      return parsedUrlName;
+    } else {
+      this.throw404();
+    }
+  },
+
+  afterModel: function(model, transition) {
+    if (this.get('usingUrlName')) {
+      var userUrlName = transition.params[transition.targetName].url_name;
+      if (this.serialize(model) !== userUrlName) {
+        return this.transitionTo(transition.targetName, model);
+      }
+    }
+  },
+
+  serialize: function(model, params) {
+    if (this.get('usingUrlName')) {
+      return { url_name: model.get('slug') + '-' + model.get('id') };
+    } else {
+      return this._super(model, params);
+    }
+  }
+});

+ 5 - 3
misago/emberapp/app/router.js

@@ -7,7 +7,6 @@ var Router = Ember.Router.extend({
 
 Router.map(function() {
   // Auth
-
   this.route('activation', { path: 'activation/' }, function() {
     this.route('activate', { path: ':user_id/:token/' });
   });
@@ -28,13 +27,16 @@ Router.map(function() {
     });
   });
 
-  // Legal
+  // User
+
+  // User
+  this.route('user', { path: 'user/:url_name/' });
 
+  // Legal
   this.route('terms-of-service', { path: 'terms-of-service/' });
   this.route('privacy-policy', { path: 'privacy-policy/' });
 
   // Error
-
   this.route('error-0', { path: 'error-0/' });
   this.route('error-403', { path: 'error-403/:reason/' });
   this.route('error-404', { path: 'error-404/' });

+ 20 - 1
misago/emberapp/app/routes/misago.js

@@ -1,5 +1,24 @@
 import Ember from 'ember';
 import DocumentTitle from 'misago/mixins/document-title';
 import ResetScroll from 'misago/mixins/reset-scroll';
+import ModelUrlName from 'misago/mixins/model-url-name';
 
-export default Ember.Route.extend(DocumentTitle, ResetScroll);
+export default Ember.Route.extend(DocumentTitle, ResetScroll, ModelUrlName, {
+  // Shorthands for raising errors
+  throw403: function(reason) {
+    if (reason) {
+      throw {
+        status: 403,
+        responseJSON: {
+          detail: reason
+        }
+      };
+    } else {
+      throw { status: 403 };
+    }
+  },
+
+  throw404: function() {
+    throw { status: 404 };
+  }
+});

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

@@ -0,0 +1,10 @@
+import MisagoRoute from 'misago/routes/misago';
+
+export default MisagoRoute.extend({
+  usingUrlName: true,
+
+  model: function(params) {
+    var urlName = this.getParsedUrlNameOr404(params.url_name);
+    return this.store.find('user', urlName.id);
+  }
+});

+ 9 - 3
misago/emberapp/app/templates/components/last-username-changes.hbs

@@ -5,7 +5,9 @@
   <li class="list-group-item">
     <div class="side-avatar hidden-md hidden-lg">
       {{#if change.changed_by}}
-        <a href="#">{{user-avatar user=change.changed_by size=42}}</a>
+        {{#link-to 'user' change.changed_by}}
+          {{user-avatar user=change.changed_by size=20}}
+        {{/link-to}}
       {{else}}
         <span>{{user-avatar user=change.changed_by size=42}}</span>
       {{/if}}
@@ -13,8 +15,12 @@
 
     <div class="first-row">
       {{#if change.changed_by}}
-      <a href="#" class="hidden-xs hidden-sm">{{user-avatar user=change.changed_by size=20}}</a>
-      <a href="#" class="item-name right-margin">{{change.changed_by.username}}</a>
+      {{#link-to 'user' change.changed_by class="hidden-xs hidden-sm"}}
+        {{user-avatar user=change.changed_by size=20}}
+      {{/link-to}}
+      {{#link-to 'user' change.changed_by class="item-name right-margin"}}
+        {{change.changed_by.username}}
+      {{/link-to}}
       {{else}}
       <span class="hidden-xs hidden-sm">{{user-avatar user=change.changed_by size=20}}</span>
       <strong class="item-name right-margin"> {{change.changed_by_username}}</strong>

+ 4 - 4
misago/emberapp/app/templates/components/user-nav.hbs

@@ -1,6 +1,6 @@
-<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
+{{#link-dropdown-toggle route='user' params=[auth.user] }}
   {{user-avatar user=auth.user size=32}}
-</a>
+{{/link-dropdown-toggle}}
 <ul class="dropdown-menu user-dropdown dropdown-menu-right" role="menu">
   <li class="user-preview">
 
@@ -25,9 +25,9 @@
 
         <div class="row">
           <div class="col-xs-6">
-            <button type="button" class="btn btn-outlined btn-default btn-block btn-sm">
+            {{#link-to 'user' auth.user activeClass="" class="btn btn-outlined btn-default btn-block btn-sm"}}
               {{gettext "Profile"}}
-            </button>
+            {{/link-to}}
           </div>
           <div class="col-xs-6">
             {{#link-to 'options' activeClass="" class="btn btn-outlined btn-default btn-block btn-sm"}}

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

@@ -0,0 +1,110 @@
+import Ember from 'ember';
+import { module, test } from 'qunit';
+import startApp from '../helpers/start-app';
+import ModelUrlName from 'misago/mixins/model-url-name';
+
+var application;
+
+module('Acceptance: Page Title Mixin', {
+  beforeEach: function() {
+    application = startApp();
+  },
+
+  afterEach: function() {
+    Ember.run(application, 'destroy');
+    Ember.$.mockjax.clear();
+  }
+});
+
+test('parseUrlName parses single word slug', function(assert) {
+  assert.expect(2);
+
+  var mixin = Ember.Object.extend(ModelUrlName).create();
+
+  var parsed = mixin.parseUrlName('lorem-123');
+  assert.equal(parsed.slug, 'lorem');
+  assert.equal(parsed.id, '123');
+});
+
+test('parseUrlName parses two words slug', function(assert) {
+  assert.expect(2);
+
+  var mixin = Ember.Object.extend(ModelUrlName).create();
+
+  var parsed = mixin.parseUrlName('lorem-ipsum-123');
+  assert.equal(parsed.slug, 'lorem-ipsum');
+  assert.equal(parsed.id, '123');
+});
+
+test('parseUrlName parses complex slugs', function(assert) {
+  assert.expect(2);
+
+  var mixin = Ember.Object.extend(ModelUrlName).create();
+
+  var parsed = mixin.parseUrlName('lorem-123-ipsum-456-78');
+  assert.equal(parsed.slug, 'lorem-123-ipsum-456');
+  assert.equal(parsed.id, '78');
+});
+
+test('parseUrlName fails to parse invalid slugs', function(assert) {
+  assert.expect(4);
+
+  var mixin = Ember.Object.extend(ModelUrlName).create();
+
+  assert.equal(mixin.parseUrlName('lorem.123'), false);
+  assert.equal(mixin.parseUrlName('lorem-123-ipsum-456-abc'), false);
+  assert.equal(mixin.parseUrlName('abc'), false);
+  assert.equal(mixin.parseUrlName('123'), false);
+});
+
+test('getParsedUrlNameOr404 passes on valid slug', function(assert) {
+  assert.expect(2);
+
+  Ember.$.mockjax({
+    url: '/api/users/42/',
+    status: 403,
+    responseText: {
+      detail: 'Server was hit by Ember!'
+    }
+  });
+
+  visit('user/misago-42/');
+
+  andThen(function() {
+    assert.equal(currentPath(), 'error-403');
+
+    var errorMessage = Ember.$.trim(find('.error-message .lead').text());
+    assert.equal(errorMessage, 'Server was hit by Ember!');
+  });
+});
+
+test('getParsedUrlNameOr404 raises error 404 on invalid slug', function(assert) {
+  assert.expect(1);
+
+  visit('user/mis+ago-42/');
+
+  andThen(function() {
+    assert.equal(currentPath(), 'error-404');
+  });
+});
+
+test('getParsedUrlNameOr404 redirects on outdated slug', function(assert) {
+  assert.expect(2);
+
+  Ember.$.mockjax({
+    url: '/api/users/42/',
+    status: 200,
+    responseText: {
+      id: '42',
+      username: 'Miasgo',
+      slug: 'miasgo'
+    }
+  });
+
+  visit('user/misago-42/');
+
+  andThen(function() {
+    assert.equal(currentPath(), 'user');
+    assert.ok(currentURL().indexOf('user/miasgo-42') !== -1);
+  });
+});

+ 19 - 2
misago/users/api/users.py

@@ -1,5 +1,6 @@
 from django.contrib.auth import get_user_model
 from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404
 from django.utils.translation import ugettext as _
 
 from rest_framework import status, viewsets
@@ -7,11 +8,13 @@ from rest_framework.decorators import detail_route
 from rest_framework.parsers import JSONParser, MultiPartParser
 from rest_framework.response import Response
 
+from misago.acl import add_acl
+
 from misago.users.rest_permissions import (BasePermission,
     IsAuthenticatedOrReadOnly, UnbannedAnonOnly)
 from misago.users.forms.options import ForumOptionsForm
 
-from misago.users.serializers import UserSerializer
+from misago.users.serializers import UserSerializer, UserProfileSerializer
 
 from misago.users.api.userendpoints.avatar import avatar_endpoint
 from misago.users.api.userendpoints.create import create_endpoint
@@ -42,11 +45,25 @@ class UserViewSet(viewsets.GenericViewSet):
     permission_classes = (UserViewSetPermission,)
     parser_classes=(JSONParser, MultiPartParser)
     serializer_class = UserSerializer
-    queryset = get_user_model().objects.all()
+    queryset = get_user_model().objects
+
+    def get_queryset(self):
+        relations = ('rank', 'online_tracker', 'ban_cache')
+        return self.queryset.select_related(*relations)
 
     def list(self, request):
         pass
 
+    def retrieve(self, request, pk=None):
+        qs = self.get_queryset()
+        profile = get_object_or_404(self.get_queryset(), id=pk)
+
+        add_acl(request.user, profile)
+
+        serializer = UserProfileSerializer(
+            profile, context={'user': request.user})
+        return Response(serializer.data)
+
     def create(self, request):
         return create_endpoint(request)
 

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

@@ -4,13 +4,16 @@ from rest_framework import serializers
 
 from misago.acl import serialize_acl
 
+from misago.users.online.utils import get_user_state
 from misago.users.serializers import RankSerializer
 
+
 __all__ = [
     'AuthenticatedUserSerializer',
     'AnonymousUserSerializer',
     'BasicUserSerializer',
     'UserSerializer',
+    'UserProfileSerializer',
 ]
 
 
@@ -66,11 +69,78 @@ class BasicUserSerializer(serializers.ModelSerializer):
 
 
 class UserSerializer(serializers.ModelSerializer):
+    rank = RankSerializer(many=False, read_only=True)
+    state = serializers.SerializerMethodField()
+    signature = serializers.SerializerMethodField()
+
     class Meta:
         model = get_user_model()
         fields = (
             'id',
             'username',
             'slug',
-            'avatar_hash'
+            'is_avatar_locked',
+            'avatar_hash',
+            'title',
+            'rank',
+            'state',
+            'is_signature_locked',
+            'signature',
         )
+
+    def get_state(self, obj):
+        return get_user_state(obj, self.context['user'].acl)
+
+    def get_signature(self, obj):
+        if obj.has_valid_signature:
+            return obj.signature.signature_parsed
+        else:
+            return None
+
+
+class UserProfileSerializer(UserSerializer):
+    email = serializers.SerializerMethodField()
+    is_followed = serializers.SerializerMethodField()
+    is_blocked = serializers.SerializerMethodField()
+    acl = serializers.SerializerMethodField()
+
+    class Meta:
+        model = get_user_model()
+        fields = (
+            'id',
+            'username',
+            'slug',
+            'email',
+            'is_avatar_locked',
+            'avatar_hash',
+            'title',
+            'rank',
+            'is_signature_locked',
+            'signature',
+            'is_followed',
+            'is_blocked',
+            'state',
+            'acl',
+        )
+
+    def get_email(self, obj):
+        if (obj == self.context['user'] or
+                self.context['user'].acl['can_see_users_emails']):
+            return obj.email
+        else:
+            return None
+
+    def get_acl(self, obj):
+        return obj.acl_
+
+    def get_is_followed(self, obj):
+        if obj.acl_['can_follow']:
+            return self.context['user'].is_following(obj)
+        else:
+            return False
+
+    def get_is_blocked(self, obj):
+        if obj.acl_['can_block']:
+            return self.context['user'].is_blocking(obj)
+        else:
+            return False