Sean Micklethwaite avatar Sean Micklethwaite committed 4627887

Linkedin login button, Patch tree

Comments (0)

Files changed (9)

srcfrk_app/api_urls.py

 user_handler = Resource(UserHandler, **ad)
 
 urlpatterns = patterns('',
+    url(r'^f(?P<fork__fork_id>[a-zA-Z0-9]+).r(?P<revision_id>[0-9]+)(.(?P<emitter_format>\w+))?$',
+        revision_handler, name='api_revision'),
     (r'^forks(.(?P<emitter_format>\w+))?$', fork_handler, {}, 'api_fork'),
     (r'^f(?P<fork_id>[a-zA-Z0-9]+)(.(?P<emitter_format>\w+))?$', fork_handler, {}, 'api_fork'),
-    (r'^f(?P<fork__fork_id>[a-zA-Z0-9]+)/revisions.(?P<emitter_format>\w+)$', revision_handler, {}, 'api_revision'),
-    (r'^f(?P<fork_id>[a-zA-Z0-9]+).r(?P<revision_id>[0-9]+).(?P<emitter_format>\w+)$', fork_handler),
+    (r'^f(?P<fork__fork_id>[a-zA-Z0-9]+)/revisions.(?P<emitter_format>\w+)$', revision_handler, {}, 'api_revisions'),
+    
     
     (r'^patch/f(?P<fork_id>[a-zA-Z0-9]+)$', patch_fork_handler),
     (r'^patch/f(?P<fork_id>[a-zA-Z0-9]+).r(?P<revision_id>[0-9]+)$', patch_fork_handler),

srcfrk_app/handlers.py

 class ForkHandler(BaseHandler):
     allowed_methods = ('GET','POST','UPDATE')
     model = models.Fork
-    fields = ('fork_id', 'description', 'absolute_uri',
+    fields = ('fork_id', 'description', 'absolute_uri', 'name', 'text', 'head',
             'owner', 'posted_on', 'modified_on', 'parent', 'score', 'tags')
+    queryset = lambda self,request: Vote.objects.get_top(self.model)
     
     @validate(forms.NewForkForm)
     def create(self, request):

srcfrk_app/models.py

 PATCHER = diff_match_patch()
 EMPTY_TRANS = string.maketrans("", "")
 
+class property_default(object):
+    def __init__(self, getter):
+        self.getter = getter
+    
+    def __get__(self, instance, owner):
+        if instance is not None:
+            v = self.getter.__get__(instance, owner)()
+            self.__set__(instance, v)
+            return v
+        return self
+    
+    def __set__(self, instance, value):
+        instance.__dict__[self.getter.__name__] = value
+    
+    def __del__(self, instance):
+        del instance.__dict__[self.getter.__name__]
+
+
 class cached_property(property):
     #TODO: invalidation on model update
     def __init__(self, *args, **kwargs):
+        self.__name = kwargs.pop('cached_name', None)
         super(cached_property, self).__init__(*args, **kwargs)
-        self.__name = '_cached_property__' + self.fget.__name__
         
+        if self.__name is None:
+            self.__name = '_cached_property__' + self.fget.__name__
+    
     def __get__(self, instance, owner):
         if instance is not None:
             v = getattr(instance, self.__name, None)
     
     votes = generic.GenericRelation(Vote)
     
+    
     def save(self, *args, **kwargs):
         self.modified_on = datetime.now()
         if not self.fork_id:
         
         super(Fork, self).save(*args, **kwargs)
     
-    def getScore(self):
-        if not hasattr(self, 'score'):
-            self.score = Vote.objects.get_score(self)['score']
-        return self.score
+    @property_default
+    def score(self):
+        return Vote.objects.get_score(self)['score']
     
     @property
     def head(self):
             self.posted_on = datetime.now()
         self.modified_on = datetime.now()
         super(Revision, self).save(*args, **kwargs)
-                
+    
+    @property_default
+    def score(self):
+        return Vote.objects.get_score(self)['score']
+      
     @property
     def is_head(self):
         return self.parent == None
         
         return self.text()
     
+    @models.permalink
+    def get_api_url(self):
+        return ('api_revision', (self.fork.fork_id,self.revision_id))
+    
     def __str__(self):
         return "%s:%s%s" % (self.fork, self.revision_id, self.is_head and " (head)" or "")
     

srcfrk_app/static/coffee/srcfrk.coffee

 			.open()
 
 
+# Function decorator
+window.requireLogin = (fn, message) ->
+	(args...) ->
+		if VIEW_MODEL.user()
+			fn.bind(this)(args...)
+		else
+			VIEW_MODEL.login((=> fn.bind(this)(args...)), message ? "You have to login before you can do that!")
+
 
 #### View Models ########
 
 	
 	search: ->
 		window.location = "/tag/" + @searchText()
-	
-	login: (action) =>
-		loginDialog = new LoginDialog()
+		
+	login: (action, message) =>
+		opts = {}
+		opts.content = message if message?
+		loginDialog = new LoginDialog(opts)
 		loginDialog.open(new LoginViewModel((user) =>
 			@user(user)
 			loginDialog.close()
 	voteDown: -> @sendVote "-1"
 	clearVote: -> @sendVote "0"
 	
-	sendVote: (v) ->
+	sendVote: requireLogin((v) ->
 		new PostRequest
 			url: "/api/vote/fork/" + @revision_slug 
 			data:
 			onSuccess: =>
 				@refresh()
 		.send()
-
+	, "Before you vote, you must login. This helps us with vote counting.")
 
 class JsonViewModel
-	constructor: (data, observables) ->
+	constructor: (data, @__observables) ->
 		for k,v of data
-			v = ko.observable(v) if k in observables
+			v = ko.observable(v) if k in @__observables
 			this[k] = v
 	
 	set: (data) =>
 		for k,v of data
-			if this[k] instanceof ko.observable
+			if k in @__observables
 				this[k](v)
 			else
 				this[k] = v
+	
+	refresh: =>
+		new Request.JSON
+			method: "GET"
+			url: @resource_uri
+			onSuccess: (data) => @set(data[0])
+		.send()
 
-class SendPatchViewModel
-	constructor: (@revision_slug, @text) ->
+
+SendPatchViewModel = new Class
+	Extends: PostRequest
+	
+	options:
+		url_base: "/api/patch/"
+	
+	initialize: (@revision_slug, @text, args...) ->
 		@description = ko.observable("")
+		@parent(args...)
 	
-	send: ->
-		new PostRequest
-			url: "/api/patch/" + @revision_slug
+	send: requireLogin((options) ->
+		# @parent() fails thanks to requireLogin
+		PostRequest::send.bind(this) Object.merge(
+			url: @options.url_base + @revision_slug
 			data:
 				description: @description()
 				text: @text
-		.send()
+		, options)
+	)
+
+CommitViewModel = new Class
+	Extends: SendPatchViewModel
+
 
 class ForkViewModel
 	constructor: (@revision_slug, @text) ->
 		@description = ko.observable("")
 	
-	send: ->
+	send: requireLogin((onSuccess) ->
 		new PostRequest
 			url: "/api/fork/" + @revision_slug
 			data:
 				description: @description()
 				text: @text
+			onSuccess: onSuccess
 		.send()
+	)
 
 class NewForkViewModel
 	constructor: ->
 					dialog.open(this)
 		.open(this)
 	
-	send: ->
+	send: requireLogin(->
 		new PostRequest
 			url: "/api/forks"
 			data:
 			onSuccess: (t) =>
 				window.location = JSON.decode(t).absolute_uri
 		.send()
+	, "Last step... just login with one of the lovely services below, and your code will be shared with the world.")
 
 
 class MergeViewModel
 	parent: null
 	
 	constructor: (data, @revision_slug, source) ->
-		super( data, ["score"] )
+		super( data, ["score", "text", "tags"] )
 		@originalSource = source
 		@source = ko.observable(source)
 		@edited = ko.observable(no)
 		@forks = ko.observableArray([])
 		@comments = ko.observableArray([])
 		@tagRequest = ko.observable(null)
+		@is_active = ko.dependentObservable(=> @revision_slug == VIEW_MODEL.revision()?.revision_slug)
 	
 		@source.subscribe( (source) =>
 			@diff = gDiff.diff_main(@originalSource, source)
 		)
 		
 		@self = this
-
-	refresh: =>
-		window.location.reload(true)
+	
+	commit: =>
+		(new CommitDialog).open(	new CommitViewModel(
+			@revision_slug, @source()
+		))
 	
 	sendPatch: =>
 		(new SendPatchDialog).open(	new SendPatchViewModel(
 			onOk: => @source(@originalSource)
 		.open()
 	
-	addTags: =>
+	addTags: => requireLogin(=>
 		tagField = $('form.tagger input[name=tags]')
 		@tagRequest(new PostRequest
 			url: '/api/tag/fork/' + @revision_slug
 			data:
 				tags: tagField.val()
-			onSuccess: => tagField.val("")
+			onSuccess: =>
+				tagField.val("")
+				@refresh()
 			onCompleted: => @tagRequest(null)
 		.send())
+	)()
 
 
 class ForkSourceViewModel extends BaseSourceViewModel
 		@revisions = ko.observableArray([])
 		@refreshRevisions()
 		
-		@patches = ko.dependentObservableArray =>
-			(r for r in @revisions() when not r.is_head and not r.is_history )
-		@editable = VIEW_MODEL?.user()?.username == @owner.username
-		@creator_name = @owner.username
+		@headRevisionModel = ko.dependentObservable(=> new RevisionSourceViewModel(@head, this))
+		
+		@patches = ko.dependentObservableArray => @headRevisionModel().patches()
+		@editable = ko.dependentObservable(=> VIEW_MODEL.user()?.username == @owner.username)
+		@creator_name = @owner.first_name + ' ' + @owner.last_name
+		@forkModel = this
+		
+		
 	
 	refreshRevisions: =>
-		@revisions.removeAll()
 		jQuery.get('/api/' + @revision_slug + '/revisions.json', {},
-			(data) => @revisions.push(new RevisionSourceViewModel(d, this)) for d in data
+			(data) => @revisions((new RevisionSourceViewModel(d, this) for d in data))
 		)
 	
 	getRevision: (id) =>
 				return r
 		throw "No such revision: " + id
 	
-		
+	view: => VIEW_MODEL.viewRevision this
+	
+	isDescendantOf: (parent) => @headRevisionModel().isDescendantOf parent
+
 
 class RevisionSourceViewModel extends BaseSourceViewModel
 	constructor: (data, @forkModel) ->
 		super( data, 'f' + @forkModel.fork_id + '.r' + data.revision_id, source )
 
 		@patches = ko.dependentObservableArray(
-			=> (r for r in forkModel.revisions() when (r.base? and r.base.revision_id == @revision_id))
+			=> (r for r in forkModel.revisions() when (r.base?.revision_id == @revision_id))
 		)
-		@creator_name = if @creator then @creator.username else "Anonymous"
+		@creator_name = if @creator then @creator.first_name + ' ' + @creator.last_name else "Anonymous"
+
+	getParent: =>
+		if not @parentModel?
+			@parentModel = @forkModel.getRevision(@parent.revision_id)
+		@parentModel
 	
 	getSource: =>
 		if not @hasSource
-			@parentModel = @forkModel.getRevision(@parent.revision_id)
-			@originalSource = gDiff.patch_apply(gDiff.patch_fromText(@diff), @parentModel.getSource())[0]
+			@originalSource = gDiff.patch_apply(gDiff.patch_fromText(@diff), @getParent().getSource())[0]
 			@source(@originalSource)
 			@hasSource = yes
 		@source()
 			VIEW_MODEL.viewRevision(@forkModel)
 	
 	viewParent: => @parentModel.view()
+	
+	isDescendantOf: (parent) =>
+		if parent instanceof ForkSourceViewModel
+			@isDescendantOf parent.headRevisionModel().revision_id
+		else if parent instanceof RevisionSourceViewModel
+			@isDescendantOf parent.revision_id
+		else if @revision_id == parent
+			true
+		else if @base?
+			@forkModel.getRevision(@base.revision_id).isDescendantOf parent
+		else
+			false
 
 
 class HomeViewModel extends SiteBaseViewModel
 		
 		@title = ko.observable("")
 		@revision = ko.observable(null)
-		@patches = ko.dependentObservableArray((-> @revision()?.patches()), this)
 	
-	init: ->
-		# Create initial source view model from unescaped source
-		@fork = new ForkSourceViewModel
-			fork_id: $('script#fork_id').text()
-			name: $('#fork_name').text()
-			text: $("<div/>").html( $('script#source').html() ).text()
-			description: $('#fork_description').text()
-			score: $('#fork_score').text()
-			owner: { username: $('#fork_owner').text() }
+	init: (@fork) ->
+		if !@fork?
+			# Create initial source view model from unescaped source
+			@fork = new ForkSourceViewModel
+				fork_id: $('script#fork_id').text()
+				name: $('#fork_name').text()
+				text: $("<div/>").html( $('script#source').html() ).text()
+				description: $('#fork_description').text()
+				score: $('#fork_score').text()
+				owner: FORK_CREATOR
+				resource_uri: FORK_URI
 		
 		@title(@fork.name)
 		@revision(@fork)
-		
+		@patches = ko.dependentObservableArray =>
+			ls = [@fork]
+			for r in @fork.revisions()
+				if !r.isDescendantOf(@fork) and !r.is_history
+					ls.push r
+			ls
+	
 	viewRevision: (r) ->
 		@revision(r)	
 		@title(@fork.name + (if r.parent then ':r' + r.revision_id else ''))
 
 window.startViewer = ->
 	window.gDiff = new diff_match_patch
-	window.VIEW_MODEL = new ViewerViewModel(USER)
-	VIEW_MODEL.init()
-	ko.applyBindings(VIEW_MODEL)
-
+	new Request.JSON
+		url: FORK_URI
+		method: "GET"
+		onSuccess: (data) ->
+			window.VIEW_MODEL = new ViewerViewModel(USER)
+			VIEW_MODEL.init(new ForkSourceViewModel data[0])
+			ko.applyBindings(VIEW_MODEL)
+	.send()
 
 
 #### Plugins

srcfrk_app/static/coffee/srcfrk.views.coffee

 		template: "descriptionTemplate"
 		onOk: -> @viewModel.send()
 
+window.CommitDialog = new Class
+	Extends: SendPatchDialog
+	options:
+		title: "Commit Changes"
+		content: "<h3>Enter a description for your changes, and click OK to post.</h3>"
+
 
 window.ForkDialog = new Class
 	Extends: SendPatchDialog

static/css/design.sass

  
 .patch
   .content
-    padding: 5px
-    float: left
-  
+    padding: 2px
+    border: 1px solid transparent
+ 
   .voting
     width: 50px
+.patch.selected > .content
+  border: 1px solid #99FF99
+  background-color: #CCFFCC
+.patch
+  li
+    padding-left: 10px
+
 
 #fork_score
   .voting
     font-size: 11px
     text-transform: uppercase
 
-.searchResults, .forks
+.searchResults, .forks, .patchTree
   margin: 0
   padding: 0
   
     background-color: #99CCFF
     float: left
     
-.forks
+.forks, .patchTree
   .score
     @extend .tinyWidget
     background-color: #99FF99
Add a comment to this file

static/img/log-in-linkedin-small.png

Added
New image

templates/site_base.html

 	
 	<script id='loginTemplate' type="text/x-jquery-tmpl">
 		<button data-bind="click: facebookLogin"><img src="{% media_url 'img/fb-connect.png' %}" /></button>
-		<button data-bind="click: linkedinLogin"><img src="{% media_url 'img/linkedin_logo.png' %}" /></button>
+		<button data-bind="click: linkedinLogin"><img src="{% media_url 'img/log-in-linkedin-small.png' %}" /></button>
 	</script>
 	
 	{% block bootstrap %}

templates/srcfrk_app/fork_detail.html

 	{% include_media "editor.js" %}
 	<script id="fork_id" type="text/plain">{{ fork.fork_id }}</script>
 	<script id="source" type="text/plain">{{ fork.text }}</script>
+	<script type="text/javascript">
+		window.FORK_CREATOR = {
+			username: "{{ fork.owner.username }}",
+			first_name: "{{ fork.owner.first_name }}",
+			last_name: "{{ fork.owner.last_name }}"
+		};
+		window.FORK_URI = "{{ fork.get_api_url }}";
+	</script>
 	
 	{% raw %}
 	<script id="patchTemplate" type="text/html">
 			<div data-bind="template: 'votingTemplate'"></div>
 			<div class="content">
 			<h3><a href="#" data-bind="click: view">r${revision_id}</a>
-			 by {{ if creator }}${creator.username}{{ else }}Anonymous{{ /if }}</h3>
+			 by {{ if creator }}${creator.first_name} ${creator.last_name}{{ else }}Anonymous{{ /if }}</h3>
 			{{ if description }}<div class="description">${description}</div>{{ /if }}
 			</div>
 		</div>
 		<div class="clear"></div>
 	</script>
+	<script id="patchTreeTemplate" type="text/html">
+		<li class="patch {{ if is_active }}selected{{/if}}">
+			<div class="content">
+				<div class="score">${score}</div>
+				<a href="#" data-bind="click: view">${revision_slug}</a>
+			 	by {{ if creator }}${creator.first_name} ${creator.last_name}{{ else }}Anonymous{{ /if }}
+			</div>
+			<ul class="patchTree" data-bind='template: { name: "patchTreeTemplate", 
+                            foreach: patches }'> </ul>
+		</li>
+		<div class="clear"></div>
+	</script>
+	
 	<script id="votingTemplate" type="text/html">
 		<div class="voting">
 			<div class="up"><a href="#" data-bind="click: voteUp">Vote Up</a></div>
 			<div class="down"><a href="#" data-bind="click: voteDown">Vote Down</a></div>
 		</div>
 	</script>
+	
+	<script id="tagTemplate" type="text/html">
+		<li>${ name }</li>
+	</script>
 	{% endraw %}
 	
 	<script id="viewerActionsTemplate" type="text/x-jquery-tmpl">
 		<button data-bind="click: showEditor">{% icon "pencil" %} Edit This</button>
 		<button onclick="$('#sourceEl').selectText()">{% icon "page_copy" %} Select All</button>
-		<button data-bind="visible: revision().parent, click: revision().merge">{% icon "arrow_merge" %} Pull Changes</button>
+		<button data-bind="visible: revision().parent && revision().forkModel.editable(), click: revision().merge">{% icon "arrow_merge" %} Pull Changes</button>
 	</script>
 	
 	<script id="parentTemplate" type="text/x-jquery-tmpl">
 <div id="contentcolumn">
 	<div class="innertube">
 		<div data-bind="template: { name: 'votingTemplate', 
-                            data: revision() }" id="fork_score">{{ fork.getScore }}</div>
+                            data: revision() }" id="fork_score">{{ fork.score }}</div>
         
-		<h1><span id="fork_name" data-bind="text: title">{{ fork.name }}</span> <span class="by">by</span> <span data-bind="text: revision().creator_name" id="fork_owner">{{ fork.owner.get_full_name }}</span></h1>
+		<h1><span id="fork_name" data-bind="text: title">{{ fork.name }}</span> <span class="by">by</span> <span data-bind="text: revision().creator_name">{{ fork.owner.get_full_name }}</span></h1>
 		<div data-bind="template: 'viewerActionsTemplate'"></div>
 		
 		<pre class="viewer"><code data-bind="text: revision().source()" id="sourceEl">{{ fork.text }}</code></pre>
 		
 		<div data-bind="visible: revision().edited()">
 			<b>You edited this file.</b>
-			<button data-bind="click: revision().sendPatch">{% icon "bandaid" %} Send Patch</button>
+			<button data-bind="visible: revision().forkModel.editable(), click: revision().commit">{% icon "disk" %} Commit Changes</button>
+			<button data-bind="visible: !revision().forkModel.editable(), click: revision().sendPatch">{% icon "bandaid" %} Send Patch</button>
 			<button data-bind="click: revision().fork">{% icon "arrow_divide" %} Fork</button>
 			<button data-bind="click: revision().cancelEdit">{% icon "cancel" %} Cancel</button>
 			<div>
 		<div data-bind="visible: revision().parent, template: {name: 'parentTemplate', data: revision()}"></div>
 		
 		<h2>Tags</h2>
-		<ul class="tags">
+		<ul class="tags" data-bind='template: { name: "tagTemplate", 
+                            foreach: fork.tags() }'>
 			{% for tag in fork.tags %}
-			<li>{{ tag }}</li>
+			<li data-bind='visible: false'>{{ tag }}</li>
 			{% endfor %}
 		</ul>
 		<form class="tagger">
 		</form>
 		
 		<h2>Patches</h2>
-		<div data-bind='template: { name: "patchTemplate", 
-                            foreach: patches }'> </div>
+		<ul class="patchTree" data-bind='template: { name: "patchTreeTemplate", 
+                            foreach: patches }'> </ul>
 		<h2>Forks</h2>
 		
 	</div>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.